Control de errores con C++/WinRT

En este tema se describen las estrategias para controlar errores al programar con C++/WinRT. Para obtener más información general y antecedentes, consulta Errores y control de excepciones (C++moderno) .

Evitar capturar y lanzar excepciones

Recomendamos que sigas escribiendo código resistente a excepciones, pero que prefieras evitar capturar y lanzar excepciones siempre que sea posible. Si no hay ningún manejador para una excepción, Windows genera automáticamente un informe de error (incluido un minivolcado del fallo), lo que le ayudará a localizar dónde está el problema.

No lance una excepción que espere capturar. Y no use excepciones para los errores esperados. Lance una excepción solo cuando ocurra un error inesperado en tiempo de ejecución, y gestione todo lo demás con códigos de error o de resultado, directamente y cerca del origen del fallo. De este modo, cuando se produce una excepción, sabe que la causa es un error en el código o un estado de error excepcional en el sistema.

Considere el escenario de acceso al Registro de Windows. Si tu aplicación no puede leer un valor del Registro, es algo esperable, y deberías gestionarlo adecuadamente. No genere una excepción; sino que devuelva un valor bool o enum que indique que el valor no se leyó y, quizás, por qué. No poder escribir un valor en el Registro, por otro lado, probablemente indique que hay un problema más grave de lo que su aplicación puede gestionar razonablemente. En un caso como este, no quiere que la aplicación continúe, por lo que una excepción que da como resultado un informe de errores es la manera más rápida de evitar que la aplicación cause ningún daño.

En otro ejemplo, considere la posibilidad de recuperar una imagen en miniatura de una llamada a StorageFile.GetThumbnailAsync y, a continuación, pasar esa miniatura a BitmapSource.SetSourceAsync. Si esa secuencia de llamadas hace que pase nullptr a SetSourceAsync (el archivo de imagen no se puede leer; quizás su extensión de archivo hace que parezca que contiene datos de imagen, pero realmente no), se producirá una excepción de puntero no válida. Si detecta un caso similar al de su código, en lugar de detectar y controlar el caso como una excepción, compruebe si nullptr se devuelve de GetThumbnailAsync.

Lanzar excepciones tiende a ser más lento que usar códigos de error. Si solo produce una excepción cuando se produce un error irrecuperable, si todo va bien, nunca pagará el precio de rendimiento.

Pero una penalización del rendimiento más probable proviene de la sobrecarga de ejecución que supone garantizar que se invoquen los destructores adecuados en el caso poco probable de que se produzca una excepción. El costo de esta garantía es si se produce o no una excepción. Por lo tanto, debe asegurarse de que el compilador tiene una buena idea de qué funciones pueden producir excepciones. Si el compilador puede demostrar que no habrá excepciones de determinadas funciones (la noexcept especificación), puede optimizar el código que genera.

Detección de excepciones

Se devuelve una condición de error que surge en la capa de ABI de Windows Runtime en forma de un valor HRESULT. Pero no necesitas manejar los HRESULT en tu código. El código de proyección de C++/WinRT que se genera para una API del lado cliente detecta un código de error HRESULT en la capa ABI y convierte dicho código en una excepción winrt::hresult_error, que puede capturar y controlar. Si realmente desea manipular los HRESULT, utilice el tipo winrt::hresult.

Por ejemplo, si el usuario elimina una imagen de la Biblioteca de imágenes mientras la aplicación recorre esa colección, la proyección genera una excepción. Y este es un caso en el que tendrá que detectar y controlar esa excepción. Este es un ejemplo de código que muestra este caso.

#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Microsoft.UI.Xaml.Media.Imaging.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Storage;
using namespace Microsoft::UI::Xaml::Media::Imaging;

IAsyncAction MakeThumbnailsAsync()
{
    auto imageFiles{ co_await KnownFolders::PicturesLibrary().GetFilesAsync() };

    for (StorageFile const& imageFile : imageFiles)
    {
        BitmapImage bitmapImage;
        try
        {
            auto thumbnail{ co_await imageFile.GetThumbnailAsync(FileProperties::ThumbnailMode::PicturesView) };
            if (thumbnail) bitmapImage.SetSource(thumbnail);
        }
        catch (winrt::hresult_error const& ex)
        {
            winrt::hresult hr = ex.code(); // HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND).
            winrt::hstring message = ex.message(); // The system cannot find the file specified.
        }
    }
}

Usa este mismo patrón en una corrutina al llamar a una función con la etiqueta co_await. Otro ejemplo de esta conversión de HRESULT en excepción es que, cuando la API de un componente devuelve E_OUTOFMEMORY, se lanza una std::bad_alloc.

Prefiere winrt::hresult_error::code cuando solo quieras consultar un código HRESULT. La función winrt::hresult_error::to_abi , por otro lado, convierte en un objeto de error COM e inserta el estado en el almacenamiento local del subproceso COM.

Lanzar excepciones

Habrá casos en los que decida que, si se produce un error en la llamada a una función determinada, la aplicación no podrá recuperarse (ya no podrá confiar en ella para funcionar de forma predecible). En el ejemplo de código siguiente se usa un valor winrt::handle como encapsulación del HANDLE devuelto por CreateEvent. A continuación, pasa el descriptor (creando a partir de él un valor bool) a la plantilla de funciones winrt::check_bool. winrt::check_bool funciona con , boolo con cualquier valor que se puede convertir a false (una condición de error) o true (una condición de éxito).

winrt::handle h{ ::CreateEvent(nullptr, false, false, nullptr) };
winrt::check_bool(bool{ h });
winrt::check_bool(::SetEvent(h.get()));

Si el valor que pasa a winrt::check_bool es false, se realiza la siguiente secuencia de acciones.

  • winrt::check_bool llama a la función winrt::throw_last_error .
  • winrt::throw_last_error llama a GetLastError para recuperar el valor de código de último error del subproceso de llamada y, a continuación, llama a la función winrt::throw_hresult .
  • winrt::throw_hresult produce una excepción mediante un objeto winrt::hresult_error (o un objeto estándar) que representa ese código de error.

Dado que Windows API notifican errores en tiempo de ejecución mediante varios tipos de valor devuelto, hay además de winrt::check_bool una serie de otras funciones auxiliares útiles para comprobar valores e iniciar excepciones.

  • winrt::check_hresult. Comprueba si el código HRESULT representa un error y, si es así, llama a winrt::throw_hresult.
  • winrt::check_nt. Comprueba si un código representa un error y, si es así, llama a winrt::throw_hresult.
  • winrt::check_pointer. Comprueba si un puntero es nulo y, si es así, llama a winrt::throw_last_error.
  • winrt::check_win32. Comprueba si un código representa un error y, si es así, llama a winrt::throw_hresult.

Puedes usar estas funciones auxiliares para tipos de código de retorno comunes, o puedes responder a cualquier condición de error y llamar a winrt::throw_last_error o winrt::throw_hresult.

Lanzar excepciones al diseñar una API

Todos los límites de la Windows Runtime Application Binary Interface (o límites ABI) deben ser noexcept, lo que significa que las excepciones nunca deben propagarse más allá de ellos. Al crear una API, siempre debe marcar el límite de ABI con la palabra clave de C++ noexcept . noexcept tiene un comportamiento específico en C++. Si una excepción de C++ alcanza un noexcept límite, el proceso producirá un error rápido con std::terminate. Ese comportamiento suele ser deseable, ya que una excepción no controlada casi siempre implica un estado desconocido en el proceso.

Dado que las excepciones no deben cruzar el límite de ABI, se devuelve una condición de error que surge en una implementación a través de la capa abi en forma de código de error HRESULT. Cuando creas una API con C++/WinRT, se genera código para convertir en un HRESULT cualquier excepción que lances en tu implementación. La función winrt::to_hresult se usa en ese código generado en un patrón similar al siguiente.

HRESULT DoWork() noexcept
{
    try
    {
        // Shim through to your C++/WinRT implementation.
        return S_OK;
    }
    catch (...)
    {
        return winrt::to_hresult(); // Convert any exception to an HRESULT.
    }
}

winrt::to_hresult controla las excepciones derivadas de std::exception y winrt::hresult_error y sus tipos derivados. En la implementación, debe preferir winrt::hresult_error o un tipo derivado para que los consumidores de la API reciban información de error enriquecida. std::exception (que se asigna a E_FAIL) se admite en caso de que las excepciones surjan del uso de la biblioteca de plantillas estándar.

Capacidad de depuración con noexcept

Como se mencionó anteriormente, una excepción de C++ que alcanza un límite produce un noexcept error rápido con std::terminate. Esto no es ideal para la depuración, ya que std::terminate suele hacer que se pierda gran parte o toda la información del error o del contexto de la excepción lanzada, especialmente cuando hay corrutinas de por medio.

Por lo tanto, esta sección trata el caso en el que el método ABI (que ha anotado correctamente con noexcept) usa co_await para llamar al código de proyección asincrónico de C++/WinRT. Le recomendamos que encapsule las llamadas al código de proyección de C++/WinRT en una winrt::fire_and_forget. Al hacerlo, se proporciona un lugar adecuado para que una excepción no controlada quede registrada correctamente como una excepción almacenada, lo que aumenta considerablemente la capacidad de depuración.

HRESULT MyWinRTObject::MyABI_Method() noexcept
{
    winrt::com_ptr<Foo> foo{ get_a_foo() };

    [/*no captures*/](winrt::com_ptr<Foo> foo) -> winrt::fire_and_forget
    {
        co_await winrt::resume_background();

        foo->ABICall();

        AnotherMethodWithLotsOfProjectionCalls();
    }(foo);

    return S_OK;
}

winrt::fire_and_forget tiene un método auxiliar unhandled_exception integrado que llama a winrt::terminate, que a su vez llama a RoFailFastWithErrorContext. Esto garantiza que cualquier contexto (excepción almacenada, código de error, mensaje de error, seguimiento inverso de la pila, etc.) se preserve, ya sea para la depuración en vivo o para un volcado post mortem. Para mayor comodidad, puede extraer la parte de tipo fire-and-forget a una función independiente que devuelva un winrt::fire_and_forget y, a continuación, llamar a esa función.

Código sincrónico

En algunos casos, el método ABI (que, de nuevo, ha anotado correctamente con noexcept) llama solo al código sincrónico. En otras palabras, nunca usa co_await, ya sea para llamar a un método de Windows Runtime asincrónico, o para cambiar entre subprocesos en primer plano y en segundo plano. En ese caso, la técnica fire_and_forget seguirá funcionando, pero no es eficiente. En su lugar, puede hacer algo parecido a esto.

HRESULT abi() noexcept try
{
    // ABI code goes here.
} catch (...) { winrt::terminate(); }

Error rápido

El código de la sección anterior sigue fallando rápidamente. Tal como está escrito, ese código no gestiona ninguna excepción. Cualquier excepción no controlada da como resultado la finalización del programa.

Pero esa forma es superior, porque garantiza la posibilidad de depuración. En raras ocasiones, es posible que desee try/catchy controle determinadas excepciones. Pero eso debería ser poco frecuente porque, como se explica en este tema, se desaconseja el uso de excepciones como mecanismo de control de flujo para las condiciones que espera.

Recuerde que es una mala idea dejar que una excepción no controlada salga de un contexto noexcept sin protección. En esas circunstancias, el entorno de ejecución de C++ hará que std::terminate termine el proceso, con lo que se perderá toda la información de excepciones almacenada que C++/WinRT registró cuidadosamente.

Assertions

En el caso de las suposiciones internas en la aplicación, hay aserciones. Prefiera static_assert para la validación en tiempo de compilación, siempre que sea posible. Para las condiciones en tiempo de ejecución, use WINRT_ASSERT con una expresión booleana. WINRT_ASSERT es una definición de macro y se expande a _ASSERTE.

WINRT_ASSERT(pos < size());

WINRT_ASSERT se compila fuera de las compilaciones de versión; en una compilación de depuración, detiene la aplicación en el depurador en la línea de código donde está la aserción.

No deberías usar excepciones en tus destructores. Por lo tanto, al menos en las compilaciones de depuración, puede declarar el resultado de llamar a una función desde un destructor con WINRT_VERIFY (con una expresión booleana) y WINRT_VERIFY_ (con un resultado esperado y una expresión booleana).

WINRT_VERIFY(::CloseHandle(value));
WINRT_VERIFY_(TRUE, ::CloseHandle(value));

API importantes