Tratamento de erros com C++/WinRT

Este tópico discute estratégias para lidar com erros ao programar com C++/WinRT. Para obter mais informações gerais e plano de fundo, consulte Erros e Tratamento de Exceções (C++Moderno).

Evite capturar e lançar 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, Windows gerará automaticamente um relatório de erro (incluindo um minidump da falha), o que ajudará você a rastrear 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 resto com códigos de erro/resultado, diretamente e perto da origem da falha. Dessa forma, quando uma exceção é gerada, você sabe que a causa é um bug em seu código ou um estado de erro excepcional no sistema.

Considere o cenário de acesso ao Registro de Windows. Se o aplicativo não conseguir ler um valor do Registro, isso é esperado, e você deve tratar isso de forma adequada. Não lance uma exceção; em vez disso, retorne um valor bool ou enum indicando isso e, talvez, o motivo pelo qual o valor não pôde ser lido. Não conseguir gravar um valor no Registro, por outro lado, provavelmente indica que há um problema maior do que seu aplicativo consegue resolver de forma adequada. Em um caso como esse, você não deseja que seu aplicativo continue, portanto, uma exceção que resulta em um relatório de erros é a maneira mais rápida de impedir que seu aplicativo cause danos.

Por outro exemplo, considere recuperar uma imagem em miniatura de uma chamada para StorageFile.GetThumbnailAsync e, em seguida, passar essa miniatura para BitmapSource.SetSourceAsync. Se essa sequência de chamadas fizer com que você passe nullptr para SetSourceAsync (o arquivo de imagem não pode ser lido; talvez sua extensão de arquivo faça parecer que contém dados de imagem, mas na verdade não o faz), então você fará com que uma exceção de ponteiro inválida seja gerada. Se você descobrir um caso como esse no seu código, em vez de capturar e tratar esse caso como uma exceção, verifique se nullptr foi retornado por GetThumbnailAsync.

Gerar exceções tende a ser mais lento 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. Portanto, você deve garantir que o compilador tenha uma boa ideia de quais funções podem potencialmente gerar exceções. Se o compilador puder provar que não haverá exceções de determinadas funções (a noexcept especificação), ele poderá otimizar o código gerado.

Capturando exceções

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

Por exemplo, se o usuário excluir uma imagem da Biblioteca de Imagens enquanto o aplicativo estiver iterando sobre essa coleção, a projeção gerará uma exceção. Nesse caso, é necessário capturar e processar essa exceção. Aqui está um exemplo de código mostrando esse 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.
        }
    }
}

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 dando uma olhada em um código 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

Haverá casos em que você decidirá que, se sua chamada para uma determinada função falhar, seu aplicativo não poderá se recuperar (você não poderá mais contar com ela para funcionar de forma previsível). O exemplo de código abaixo usa um valor winrt::handle como um wrapper em torno do HANDLE 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ê fornecer para winrt::check_bool for falso, ocorrerá a seguinte sequência de ações.

Como Windows APIs relatam erros de tempo de execução usando vários tipos de valor retornado, há além de winrt::check_bool um punhado de outras funções auxiliares úteis para verificar 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, nesse caso, 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, nesse caso, chama winrt::throw_hresult.

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

Lançando exceções ao criar uma API

Todas as fronteiras da Interface Binária de Aplicativo do Windows Runtime (ou fronteiras da ABI) devem ser noexcept — o que significa que as exceções nunca devem se propagar além delas. Ao criar uma API, você sempre deve marcar o limite da ABI com a palavra-chave C++ noexcept . noexcept tem um comportamento específico em C++. Se uma exceção C++ atingir um noexcept limite, o processo falhará rapidamente com std::terminate. Esse comportamento geralmente é desejável, pois uma exceção sem tratamento quase sempre implica um estado desconhecido no processo.

Como as exceções não devem cruzar o limite da ABI, uma condição de erro que surge em uma implementação é retornada na camada 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 nesse código gerado em um padrão como 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 manipula exceções derivadas de std::exception e winrt::hresult_error e seus tipos derivados. Em sua implementação, você deve preferir winrt::hresult_error ou um tipo derivado, para que os consumidores de sua API recebam informações de erro avançadas. Há suporte para std::exception (que mapeia para E_FAIL) caso ocorram exceções decorrentes do uso da Standard Template Library (STL).

Capacidade de depuração com noexcept

Como mencionamos acima, uma exceção C++ atingindo um noexcept limite 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.

Então, esta seção trata do caso em que seu método ABI (que você anotou corretamente com noexcept) usa co_await para chamar código assíncrono de projeção 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 unhandled_exception método 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 ABI (que, novamente, você anotou corretamente com noexcept) chama apenas 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 threads de primeiro plano e de plano de fundo. Nesse caso, a técnica fire_and_forget ainda funcionará, mas não será eficiente. Em vez disso, você pode 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 lida com exceções. Qualquer exceção sem tratamento resulta no encerramento do programa.

Mas esse formato é superior, pois garante a capacidade de depuração. Em casos raros, talvez você queira try/catch e lidar com determinadas exceções. Mas isso deve ser raro porque, como este tópico explica, desencorajamos 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.

Assertions

Para suposições internas em seu aplicativo, existem asserções. Prefira static_assert para validação em 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