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

Recomendamos que você continue escrevendo código seguro em relação a exceções, mas que prefira evitar capturar e lançar 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ê espera 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. Se você lançar uma exceção apenas quando ocorrer um erro fatal, então, se tudo correr bem, nunca pagará o custo de desempenho.

Mas um impacto no desempenho mais provável envolve a sobrecarga em tempo de execução para garantir que os destrutores apropriados sejam chamados na eventualidade improvável de uma exceção ser gerada. O custo dessa garantia vem se uma exceção é realmente 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. E este é um caso em que você terá que pegar e lidar com 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 marcada com co_await. Outro exemplo dessa conversão de HRESULT em exceção é que, quando uma API de componente retorna E_OUTOFMEMORY, isso faz com que a exceção std::bad_alloc seja lançada.

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, converte para um objeto de erro COM e coloca o estado no armazenamento local de thread do COM.

Lançando 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, ele passa o manipulador (criando um valor bool a partir dele) para o template 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. Quando você está criando uma API usando C++/WinRT, o código é gerado para você converter qualquer exceção que você lançar em sua 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, pois std::terminate geralmente perde muito ou todo o erro ou o contexto de exceção gerado, especialmente quando as coroutinas 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. É recomendável encapsular as chamadas para o código de projeção C++/WinRT em um winrt::fire_and_forget. Ao fazer isso, é fornecido um local apropriado para que uma exceção não tratada seja devidamente registrada como uma exceção armazenada, o que facilita muito a 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, 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 pós-morte. Por conveniência, você pode fatorar a parte fire-and-forget em uma função separada que retorna um winrt::fire_and_forget e, em seguida, chamá-la.

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 de fire_and_forget ainda funcionará, mas não é eficiente. Em vez disso, você pode fazer algo assim.

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

Falhe rápido

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 essa forma é superior, porque permite a 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 má ideia deixar uma exceção sem tratamento escapar de um contexto vazio noexcept. Sob essa condição, o runtime do C++ encerrará o processo, perdendo assim todas as informações de exceção armazenadas que C++/WinRT registrou cuidadosamente.

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 é eliminado na compilação em builds de release; em um build de depuração, ele interrompe o aplicativo no depurador na linha de código em que a asserção se encontra.

Você não deve usar exceções em seus destrutores. Portanto, pelo menos em compilações de depuração, você pode verificar o resultado da chamada de uma função no destrutor com WINRT_VERIFY (com uma expressão booleana) e WINRT_VERIFY_ (com um resultado esperado e uma expressão booleana).

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

APIs importantes