Processamento de erros com C++/WinRT

Este tópico aborda as estratégias para processar erros ao programar com C++/WinRT. Para obter informações gerais e o histórico, veja Processamento de erros e exceções (C++ moderno).

Evitar a captura e a geração de exceções

É recomendável continuar escrevendo código à prova de exceções, mas evite a captura e a geração de exceções sempre que possível. Se não houver nenhum manipulador para uma exceção, o Windows vai gerar automaticamente um relatório de erros (incluindo um minidespejo da falha), que ajudará você a detectar onde está o problema.

Não gere uma exceção que você pretende capturar. E não use exceções para falhas esperadas. Gere uma exceção somente quando ocorrer um erro de runtime inesperado e manipule todo o restante com códigos de erro/resultado diretamente, e feche a origem da falha. Dessa forma, quando uma exceção for gerada, você saberá que a causa é um bug no código ou um estado de erro excepcional no sistema.

Considere o cenário de acesso ao Registro do Windows. Se o aplicativo falhar ao ler um valor no Registro, isso era esperado, e você deve tratar a situação normalmente. Não gere uma exceção; em vez disso, retorne um valor bool ou enum, indicando que, e talvez por que, o valor não foi lido. Por outro lado, a falha ao gravar um valor no Registro, provavelmente indica que há um problema maior que você pode processar de maneira perceptível em seu aplicativo. Em casos assim, não é recomendado que o aplicativo continue. Portanto, uma exceção que resulta em um relatório de erros é a maneira mais rápida de impedir que o aplicativo cause danos.

Em outro exemplo, considere recuperar uma imagem em miniatura de uma chamada a StorageFile.GetThumbnailAsync e passá-la para BitmapSource.SetSourceAsync. Se essa sequência de chamadas resultar em passar nullptr para SetSourceAsync (o arquivo de imagem não pode ser lido; talvez sua extensão faça parecer que ele contém dados de imagem, mesmo não contendo), você provocará a geração de uma exceção de ponteiro inválido. Se você descobrir um caso semelhante no seu código, em vez de capturar e processar o caso como uma exceção, verifique se nullptr foi retornado de GetThumbnailAsync.

A geração de exceções tende a ser mais lenta do que usar códigos de erro. Caso uma exceção seja gerada somente quando um erro fatal ocorrer, e se tudo correr bem, você nunca terá um problema de desempenho.

Mas um impacto mais provável no desempenho envolve a sobrecarga do runtime ao garantir que os destruidores apropriados sejam chamados no evento improvável de geração da exceção. O custo dessa garantia é percebido não importando se uma exceção é de fato gerada ou não. Assim, você deve garantir que o compilador tenha uma boa noção sobre quais funções podem potencialmente gerar exceções. Se o compilador puder provar que não haverá qualquer exceção nas funções específicas (a especificação noexcept), é possível otimizar o código gerado.

Capturando exceções

Uma condição de erro que surge na camada ABI do Windows Runtime é retornada na forma de um valor HRESULT. Mas você não precisa processar HRESULTs em seu código. O código de projeção do C++/WinRT gerado para uma API no lado de consumo detecta um código de erro de HRESULT na camada ABI e converte o código em uma exceção winrt::hresult_error, que você pode capturar e processar. Se você realmente desejar processar HRESULTS, use o tipo winrt::hresult.

Por exemplo, se o usuário excluir uma imagem da biblioteca de imagens enquanto o aplicativo estiver iterando nessa coleção, a projeção vai gerar uma exceção. Nesse caso, é necessário capturar e processar essa exceção. Veja a seguir um exemplo de código mostrando esse caso.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Storage;
using namespace Windows::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.
        }
    }
}

Use esse mesmo padrão em uma corrotina ao chamar uma função co_await-ed. Outro exemplo dessa conversão de HRESULT em exceção é que, quando uma API de componente retorna E_OUTOFMEMORY, isso gera um std::bad_alloc.

Prefira winrt::hresult_error::code quando você estiver apenas espiando um código do HRESULT. A função winrt::hresult_error::to_abi, por outro lado, é convertida em um objeto de erro COM e conduz o estado para o armazenamento local de thread COM.

Acionamento de exceções

Há casos em que você deve decidir que, em caso de falha na chamada para uma função específica, o aplicativo não será capaz de se recuperar (não é possível confiar no funcionamento previsível). O exemplo de código abaixo usa um valor de winrt::handle como um wrapper em torno do IDENTIFICADOR retornado de CreateEvent. Em seguida, passa o identificador (criando um valor bool a partir dele) para o modelo de função winrt::check_bool. winrt::check_bool funciona com um bool ou com qualquer valor que possa ser convertido em false (uma condição de erro) ou true (uma condição de sucesso).

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

Se o valor que você passar para winrt::check_bool for false, ocorrerá a sequência de ações a seguir.

Como as APIs do Windows relatam erros em tempo de execução usando vários tipos de valor de retorno, existem algumas outras funções auxiliares úteis além de winrt::check_bool para verificar os valores e gerar exceções.

  • winrt::check_hresult. Verifica se o código HRESULT representa um erro e, em caso afirmativo, chama winrt::throw_hresult.
  • winrt::check_nt. Verifica se um código representa um erro e, em caso afirmativo, chama winrt::throw_hresult.
  • winrt::check_pointer. Verifica se um ponteiro é nulo e, em caso afirmativo, chama winrt::throw_last_error.
  • winrt::check_win32. Verifica se um código representa um erro e, em caso afirmativo, chama winrt::throw_hresult.

Você pode usar essas funções auxiliares para tipos comuns de código de retorno ou pode responder a qualquer condição de erro e chamar winrt::throw_last_error ou winrt::throw_hresult.

Gerando exceções ao criar uma API

Todos os limites da Interface Binária de Aplicativo do Windows Runtime (ou limites do ABI) precisam ser noexcept – o que significa que as exceções nunca devem fazer escape nesse local. Ao criar uma API, você sempre deverá marcar o limite do ABI com a palavra-chave noexcept do C++. noexcept tem um comportamento específico no C++. Se uma exceção do C++ atingir um limite noexcept, o processo falhará rapidamente com std::terminate. Esse comportamento é geralmente desejável, porque uma exceção sem tratamento quase sempre implica um estado desconhecido no processo.

Como as exceções não devem cruzar o limite do ABI, uma condição de erro acionada em uma implementação é retornada na camada do ABI na forma de um código de erro HRESULT. Ao criar uma API usando C++/WinRT, o código é gerado para você converter qualquer exceção gerada na implementação em um HRESULT. A função winrt::to_hresult é usada no código gerado em um padrão semelhante a este.

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 processa as exceções derivadas de std::exception, winrt::hresult_error e seus tipos derivados. Na implementação, você deve preferir winrt::hresult_error, ou um tipo derivado, para que os consumidores da API recebam informações de erro detalhadas. std::exception (mapeada para E_FAIL) será aceita caso surjam exceções devido ao uso da Biblioteca de Modelos Padrão.

Capacidade de depuração com noexcept

Como mencionamos acima, uma exceção do C++ que atinge um limite noexcept falha rapidamente com std::terminate. Isso não é ideal para depuração, porque std::terminate muitas vezes perde grande parte ou todo o contexto de erro ou de exceção gerado, especialmente quando as corrotinas estão envolvidas.

Portanto, esta seção trata do caso em que o método do ABI (que você anotou corretamente com noexcept) usa co_await para chamar o código de projeção assíncrona do C++/WinRT. Recomendamos que você encapsule as chamadas ao código de projeção do C++/WinRT em um winrt::fire_and_forget. Essa ação fornece um local adequado para que uma exceção sem tratamento seja registrada corretamente como uma exceção recolhida, o que aumenta significativamente a capacidade de depuração.

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 tem um auxiliar de método unhandled_exception interno, que chama winrt::terminate, que, por sua vez, chama RoFailFastWithErrorContext. Isso garante que qualquer contexto (exceção recolhida, código de erro, mensagem de erro, backtrace de pilha e assim por diante) seja preservado para depuração dinâmica ou para um despejo post-mortem. Para sua conveniência, você pode fatorar a parte de acionamento e esquecimento em uma função separada que retorna um winrt::fire_and_forget e, em seguida, chama isso.

Código síncrono

Em alguns casos, o método do ABI (que, mais uma vez, você anotou corretamente com noexcept) chama apenas o código síncrono. Em outras palavras, ele nunca usa co_await, seja para chamar um método assíncrono do Windows Runtime, seja para alternar entre os threads de primeiro e segundo plano. Nesse caso, a técnica fire_and_forget ainda funcionará, mas não será eficiente. Em vez disso, você poderá fazer algo assim.

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

Fail fast

O código na seção anterior ainda falha rapidamente. Conforme escrito, esse código não trata nenhuma exceção. Qualquer exceção sem tratamento resulta na terminação do programa.

Mas esse formato é superior, pois garante a capacidade de depuração. Em casos raros, talvez você deseje aplicar try/catch e tratar determinadas exceções. Mas isso deve ser raro porque, como este tópico explica, não recomendamos o uso de exceções como um mecanismo de controle de fluxo para condições esperadas.

Lembre-se de que é uma boa ideia deixar uma exceção sem tratamento fazer escape de um contexto noexcept naked. Sob essa condição, o runtime C++ usará std::terminate para terminar o processo, perdendo todas as informações de exceção recolhidas cuidadosamente registradas pelo C++/WinRT.

Asserções

Para suposições internas em seu aplicativo, existem asserções. Prefira static_assert para a validação de tempo de compilação sempre que possível. Para condições de tempo de execução, use WINRT_ASSERT com uma expressão booliana. WINRT_ASSERT é uma definição de macro e se expande para _ASSERTE.

WINRT_ASSERT(pos < size());

WINRT_ASSERT é compilado nas compilações lançadas; em uma compilação de depuração, ele interrompe o aplicativo no depurador na linha de código onde está a asserção.

Você não deve usar exceções em seu destruidores. Portanto, pelo menos nas compilações de depuração, você pode declarar o resultado da chamada de uma função a partir de um destrutor com WINRT_VERIFY (com uma expressão booliana) e WINRT_VERIFY_ (com um resultado esperado e uma expressão booliana).

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

APIs importantes