Compartir a través de


Control de errores en COM (Introducción a Win32 y C++)

COM usa valores HRESULT para indicar el éxito o error de una llamada de método o función. Varios encabezados del SDK definen varias constantes HRESULT . Un conjunto común de códigos para todo el sistema se define en WinError.h. En la tabla siguiente se muestran algunos de esos códigos de retorno para todo el sistema.

Constante Valor numérico Descripción
E_ACCESSDENIED 0x80070005 Acceso denegado.
E_FAIL 0x80004005 Error no especificado.
E_INVALIDARG 0x80070057 Valor de parámetro no válido.
E_OUTOFMEMORY 0x8007000E Memoria insuficiente
E_POINTER 0x80004003 NULL se pasó incorrectamente para un valor de puntero.
E_UNEXPECTED 0x8000FFFF Condición inesperada.
S_OK 0x0 Correcto.
S_FALSE 0x1 Correcto.

 

Todas las constantes con el prefijo "E_" son códigos de error. Las constantes S_OK y S_FALSE son códigos de éxito. Probablemente el 99 % de los métodos COM devuelvan S_OK cuando se realizan correctamente; pero no dejes que este hecho te malgase. Un método puede devolver otros códigos de operación correcta, por lo que siempre se comprueba si hay errores mediante la macro SUCCEEDED o FAILED . En el código de ejemplo siguiente se muestra la manera incorrecta y la manera correcta de probar el éxito de una llamada de función.

// Wrong.
HRESULT hr = SomeFunction();
if (hr != S_OK)
{
    printf("Error!\n"); // Bad. hr might be another success code.
}

// Right.
HRESULT hr = SomeFunction();
if (FAILED(hr))
{
    printf("Error!\n"); 
}

El código de éxito S_FALSE merece mención. Algunos métodos usan S_FALSE para significar, aproximadamente, una condición negativa que no es un error. También puede indicar un "no-op": el método se realizó correctamente, pero no tuvo ningún efecto. Por ejemplo, la función CoInitializeEx devuelve S_FALSE si la llama una segunda vez desde el mismo subproceso. Si necesita diferenciar entre S_OK y S_FALSE en el código, debe probar el valor directamente, pero seguir usando FAILED o SUCCEEDED para controlar los casos restantes, como se muestra en el código de ejemplo siguiente.

if (hr == S_FALSE)
{
    // Handle special case.
}
else if (SUCCEEDED(hr))
{
    // Handle general success case.
}
else
{
    // Handle errors.
    printf("Error!\n"); 
}

Algunos valores HRESULT son específicos de una característica o subsistema concretos de Windows. Por ejemplo, la API de gráficos de Direct2D define el código de error D2DERR_UNSUPPORTED_PIXEL_FORMAT, lo que significa que el programa usó un formato de píxel no admitido. La documentación de MSDN a menudo proporciona una lista de códigos de error específicos que un método podría devolver. Sin embargo, no debe considerar que estas listas sean definitivas. Un método siempre puede devolver un valor HRESULT que no aparece en la documentación. De nuevo, use las macros SUCCEEDED y FAILED . Si prueba un código de error específico, incluya también un caso predeterminado.

if (hr == D2DERR_UNSUPPORTED_PIXEL_FORMAT)
{
    // Handle the specific case of an unsupported pixel format.
}
else if (FAILED(hr))
{
    // Handle other errors.
}

Patrones para el control de errores

En esta sección se examinan algunos patrones para controlar los errores COM de una manera estructurada. Cada patrón tiene ventajas y desventajas. En cierta medida, la elección es cuestión de sabor. Si trabaja en un proyecto existente, es posible que ya tenga instrucciones de codificación que proscriban un estilo determinado. Independientemente del patrón que adopte, el código sólido obedecerá las reglas siguientes.

  • Para cada método o función que devuelve un HRESULT, compruebe el valor devuelto antes de continuar.
  • Libere los recursos después de que se usen.
  • No intente acceder a recursos no válidos o no inicializados, como punteros NULL .
  • No intente usar un recurso después de liberarlo.

Teniendo en cuenta estas reglas, estos son cuatro patrones para controlar los errores.

Ifs anidados

Después de cada llamada que devuelve un HRESULT, use una instrucción if para comprobar si se ha realizado correctamente. A continuación, coloque la siguiente llamada al método dentro del ámbito de la instrucción if . Más si las instrucciones se pueden anidar tan profundamente como sea necesario. Los ejemplos de código anteriores de este módulo han usado este patrón, pero aquí es de nuevo:

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->Show(NULL);
        if (SUCCEEDED(hr))
        {
            IShellItem *pItem;
            hr = pFileOpen->GetResult(&pItem);
            if (SUCCEEDED(hr))
            {
                // Use pItem (not shown). 
                pItem->Release();
            }
        }
        pFileOpen->Release();
    }
    return hr;
}

Ventajas

  • Las variables se pueden declarar con un ámbito mínimo. Por ejemplo, pItem no se declara hasta que se usa.
  • Dentro de cada instrucción if , se cumplen ciertas invariables: todas las llamadas anteriores se han realizado correctamente y todos los recursos adquiridos siguen siendo válidos. En el ejemplo anterior, cuando el programa alcanza la instrucción if más interna, se sabe que pItem y pFileOpen son válidos.
  • Está claro cuándo liberar punteros de interfaz y otros recursos. Libera un recurso al final de la instrucción if que sigue inmediatamente a la llamada que adquirió el recurso.

Inconvenientes

  • Algunas personas encuentran difícil de leer el anidamiento profundo.
  • El control de errores se mezcla con otras instrucciones de bifurcación y bucle. Esto puede hacer que la lógica general del programa sea más difícil de seguir.

Ifs en cascada

Después de cada llamada de método, use una instrucción if para comprobar si se ha realizado correctamente. Si el método se realiza correctamente, coloque la siguiente llamada al método dentro del bloque if . Pero en lugar de anidar más instrucciones if , coloque cada prueba SUCCEEDED posterior después del bloque if anterior. Si se produce un error en cualquier método, todas las pruebas SUCCEEDED restantes simplemente producen un error hasta que se alcanza la parte inferior de la función.

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen = NULL;
    IShellItem *pItem = NULL;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));

    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->Show(NULL);
    }
    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->GetResult(&pItem);
    }
    if (SUCCEEDED(hr))
    {
        // Use pItem (not shown).
    }

    // Clean up.
    SafeRelease(&pItem);
    SafeRelease(&pFileOpen);
    return hr;
}

En este patrón, liberará los recursos al final de la función. Si se produce un error, algunos punteros podrían no ser válidos cuando se cierra la función. La llamada a Release en un puntero no válido bloqueará el programa (o peor), por lo que debe inicializar todos los punteros a NULL y comprobar si son NULL antes de liberarlos. En este ejemplo se usa la SafeRelease función ; los punteros inteligentes también son una buena opción.

Si usa este patrón, debe tener cuidado con las construcciones de bucle. Dentro de un bucle, interrumpa el bucle si se produce un error en alguna llamada.

Ventajas

  • Este patrón crea menos anidamiento que el patrón "ifs anidado".
  • El flujo de control general es más fácil de ver.
  • Los recursos se liberan en un punto del código.

Inconvenientes

  • Todas las variables se deben declarar e inicializar en la parte superior de la función.
  • Si se produce un error en una llamada, la función realiza varias comprobaciones de errores innecesarias, en lugar de salir de la función inmediatamente.
  • Dado que el flujo de control continúa a través de la función después de un error, debe tener cuidado en todo el cuerpo de la función para no tener acceso a recursos no válidos.
  • Los errores dentro de un bucle requieren un caso especial.

Saltar al producir un error

Después de cada llamada al método, pruebe si se produce un error (no es correcto). En caso de error, vaya a una etiqueta cerca de la parte inferior de la función. Después de la etiqueta, pero antes de salir de la función, libere los recursos.

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen = NULL;
    IShellItem *pItem = NULL;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFileOpen->Show(NULL);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFileOpen->GetResult(&pItem);
    if (FAILED(hr))
    {
        goto done;
    }

    // Use pItem (not shown).

done:
    // Clean up.
    SafeRelease(&pItem);
    SafeRelease(&pFileOpen);
    return hr;
}

Ventajas

  • El flujo de control general es fácil de ver.
  • En todos los puntos del código después de una comprobación FAILED , si no ha saltado a la etiqueta, se garantiza que todas las llamadas anteriores se hayan realizado correctamente.
  • Los recursos se liberan en un solo lugar del código.

Inconvenientes

  • Todas las variables se deben declarar e inicializar en la parte superior de la función.
  • A algunos programadores no les gusta usar goto en su código. (Sin embargo, debe tenerse en cuenta que este uso de goto está muy estructurado; el código nunca salta fuera de la llamada de función actual).
  • las instrucciones goto omiten los inicializadores.

Iniciar en caso de error

En lugar de saltar a una etiqueta, puede producir una excepción cuando se produce un error en un método. Esto puede producir un estilo más idiomático de C++ si está acostumbrado a escribir código seguro para excepciones.

#include <comdef.h>  // Declares _com_error

inline void throw_if_fail(HRESULT hr)
{
    if (FAILED(hr))
    {
        throw _com_error(hr);
    }
}

void ShowDialog()
{
    try
    {
        CComPtr<IFileOpenDialog> pFileOpen;
        throw_if_fail(CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
            CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen)));

        throw_if_fail(pFileOpen->Show(NULL));

        CComPtr<IShellItem> pItem;
        throw_if_fail(pFileOpen->GetResult(&pItem));

        // Use pItem (not shown).
    }
    catch (_com_error err)
    {
        // Handle error.
    }
}

Observe que en este ejemplo se usa la clase CComPtr para administrar punteros de interfaz. Por lo general, si el código produce excepciones, debe seguir el patrón RAII (La adquisición de recursos es inicialización). Es decir, todos los recursos deben administrarse mediante un objeto cuyo destructor garantiza que el recurso se libere correctamente. Si se produce una excepción, se garantiza que se invoca el destructor. De lo contrario, el programa podría perder recursos.

Ventajas

  • Compatible con el código existente que usa el control de excepciones.
  • Compatible con las bibliotecas de C++ que inician excepciones, como la biblioteca de plantillas estándar (STL).

Inconvenientes

  • Requiere objetos de C++ para administrar recursos, como identificadores de memoria o archivos.
  • Requiere una buena comprensión de cómo escribir código seguro para excepciones.

Siguientes

Módulo 3. Gráficos de Windows