Compartir a través de


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

COM usa valores HRESULT para indicar si se ha realizado o no una llamada de método o función. Varios encabezados del SDK definen diferentes constantes HRESULT. El conjunto común de códigos de todo el sistema se define en WinError.h. En la tabla siguiente se muestran algunos de esos códigos de devolución 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 ha pasado incorrectamente en 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 realicen correctamente; pero no deje que esto le engañe. Un método puede devolver otros códigos de éxito, por lo que siempre se comprueban los errores mediante la macro SUCCEEDED o FAILED. En el código de ejemplo siguiente se ver la forma incorrecta y la forma correcta de probar que se ha realizado bien 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"); 
}

Merece que se comente el código de éxito S_FALSE. Algunos métodos usan S_FALSE para señalar más o menos una condición negativa que no es un error. También puede indicar un "no-op" (no operativo): el método se ha realizado correctamente, pero no ha tenido efecto. Por ejemplo, la función CoInitializeEx devuelve S_FALSE si se llama una segunda vez a través del 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 controlar las situaciones restantes, tal como se muestra en el código de ejemplo a continuación.

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 Direct2D define el código de error D2DERR_UNSUPPORTED_PIXEL_FORMAT, lo que significa que el programa ha usado un formato de píxeles no admitido. La documentación de Windows suele venir con una lista de códigos de error específicos que podría devolver un método. Sin embargo, no debe suponer que estas listas son definitivas. Un método siempre puede devolver un valor HRESULT que no aparezca en la documentación. De nuevo, utilice 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 el control de errores COM de forma estructurada. Cada patrón tiene sus ventajas y desventajas. En cierta medida, la elección es cuestión de gustos. Si trabaja en un proyecto existente, es posible que ya tenga una serie de reglas de codificación que restrinjan un estilo determinado. Independientemente del patrón que elija, un código sólido obedecerá las siguientes reglas.

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

Si tiene presentes estas reglas, estos son los cuatro patrones para controlar errores.

If anidados

Después de cada llamada que devuelve un HRESULT, use una instrucción if para probar si se ha realizado correctamente. A continuación, coloque la siguiente llamada de método dentro de la sección de la instrucción if. Se pueden anidar más instrucciones if tanto como sea necesario. Los ejemplos de código anteriores de este módulo han usado este patrón, pero aquí lo presentamos 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, algunas invariables son true: todas las llamadas anteriores se han realizado correctamente y todos los recursos obtenidos siguen siendo válidos. En el ejemplo anterior, cuando el programa alcanza la instrucción if más interna, se constata que tanto pItem como 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 obtuvo el recurso.

Inconvenientes

  • Algunas personas creen que el anidado complejo es difícil de leer.
  • El control de errores se mezcla con otras instrucciones de ramificación y bucles. Esto puede hacer que la lógica general del programa sea más difícil de seguir.

If en cascada

Después de cada llamada de método, use una instrucción if para probar si se ha realizado correctamente. Si el método se realiza correctamente, coloque la siguiente llamada de 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 cualquiera de los métodos falla, todas las pruebas SUCCEEDED restantes tendrán un error hasta que se alcance la última parte 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, los recursos se liberan al final de la función. Si se produce un error, algunos punteros podrían no ser válidos al cerrarse la función. Al llamar a Release en un puntero no válido, se bloqueará el programa (o peor), por lo que deberá inicializar todos los punteros a NULL y comprobar si son NULL antes de liberarlos. En este ejemplo se usa la función SafeRelease; 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 anidamientos que el patrón de "if anidados".
  • El flujo general de control es más fácil de ver.
  • Los recursos se liberan en un punto del código.

Inconvenientes

  • Todas las variables deben declararse e inicializarse en la primera parte 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 tras 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 tratamiento especial.

Salto en caso de error (Jump on Fail)

Después de cada llamada de método, pruebe si hay un error (sin éxito). En caso de error, salte a una etiqueta cerca de la última parte 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 general de control es fácil de ver.
  • En todos los puntos del código después de una comprobación FAILED, si no se ha saltado a la etiqueta, se garantiza que todas las llamadas anteriores se han realizado correctamente.
  • Los recursos se liberan en una ubicación del código.

Inconvenientes

  • Todas las variables deben declararse e inicializarse en la primera parte de la función.
  • Algunos programadores prefieren no usar goto en el código. (Sin embargo, debe tenerse en cuenta que el uso de goto está muy estructurado; el código nunca salta fuera de la llamada de función actual).
  • Las instrucciones goto ignoran los inicializadores.

Excepción en caso de error (Throw on Fail)

En lugar de saltar a una etiqueta, puede producir una excepción cuando surge un error en un método. Esto puede producir un estilo más idiomático de C++ si se usa para escribir código protegido frente a 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.
    }
}

Fíjese en 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 o Resource Acquisition is Initialization (la adquisición de recursos es la inicialización). Es decir, todos los recursos deben administrarse mediante un objeto cuyo destructor garantice que el recurso se libere correctamente. Si se produce una excepción, se garantiza que se invoque 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 producen excepciones, como la biblioteca de plantillas estándar (STL).

Inconvenientes

  • Se necesitan objetos de C++ para administrar recursos como identificadores de memoria o de archivos.
  • Es necesario conocer bien cómo escribir código protegido frente a excepciones.

Siguientes

Módulo 3. Gráficos de Windows