Tratamento de erros com C++/WinRT

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

Evite exceções de apanhar e lançar

Recomendamos que continue a escrever código à prova de exceções, mas que prefira evitar capturar e lançar exceções sempre que possível. Se não houver um handler para uma exceção, o Windows gera automaticamente um relatório de erro (incluindo um minidump do crash), que te ajudará a localizar onde está o problema.

Não lance uma exceção que espera capturar. E não uses exceções para falhas esperadas. Lance uma exceção apenas quando ocorrer um erro inesperado durante a execução e trate tudo o resto com códigos de erro/resultado — diretamente e próximo da origem da falha. Assim, quando uma exceção é lançada, sabes que a causa é ou um bug no teu código, ou um estado de erro excecional no sistema.

Considere o cenário de aceder ao Registo do Windows. Se a tua aplicação não conseguir ler um valor do Registo, isso é de esperar e deves lidar com isso com elegância. Não gere uma exceção; em vez disso, devolva um valor bool ou enum que indique isso e, talvez, porquê o valor não foi lido. Por outro lado, não escrever um valor no Registo provavelmente indica que há um problema maior do que consegue resolver de forma sensata na sua aplicação. Num caso desses, não quer que a sua candidatura continue, por isso uma exceção que resulte num relatório de erro é a forma mais rápida de evitar que a sua candidatura cause qualquer dano.

Para outro exemplo, considere recuperar uma imagem em miniatura de uma chamada para o StorageFile.GetThumbnailAsync e depois passar essa miniatura para BitmapSource.SetSourceAsync. Se essa sequência de chamadas fizer com que passes nullptr para o SetSourceAsync (o ficheiro de imagem não pode ser lido; talvez a extensão do ficheiro faça parecer que contém dados de imagem, mas na verdade não tem), então vais causar uma exceção de ponteiro inválida. Se descobrires um caso assim no teu código, em vez de capturares e tratares essa situação como uma exceção, verifica antes se nullptr é devolvido por GetThumbnailAsync.

Lançar exceções tende a ser mais lento do que usar códigos de erro. Se só lançar uma exceção quando ocorrer um erro fatal, se tudo correr bem, nunca pagará o preço de desempenho.

Mas um impacto no desempenho mais provável envolve a sobrecarga em tempo de execução de garantir que os destrutores adequados sejam chamados no caso improvável de ser lançada uma exceção. O custo desta garantia reside quer uma exceção seja efetivamente feita ou não. Por isso, deve garantir que o compilador tem uma boa noção de que funções podem potencialmente lançar exceções. Se o compilador conseguir provar que não haverá exceções de certas funções (a noexcept especificação), então pode otimizar o código que gera.

Captura de exceções

Uma condição de erro que surge na camada ABI do Windows Runtime é devolvida sob a forma de um valor HRESULT. Mas não precisas de lidar com HRESULTs no teu código. O código de projeção C++/WinRT gerado para uma API no lado consumidor deteta um código HRESULT de erro na camada ABI e converte o código numa exceção winrt::hresult_error , que podes detetar e gerir. Se quiser lidar com HRESULTs, então use o tipo winrt::hresult .

Por exemplo, se o utilizador apagar uma imagem da Biblioteca de Imagens enquanto a sua aplicação está a iterar sobre essa coleção, a projeção lança uma exceção. E este é um caso em que terá de detetar e tratar dessa exceção. Aqui está um exemplo de código que mostra 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.
        }
    }
}

Use este mesmo padrão numa corrotina ao chamar uma função com co_await. Outro exemplo desta conversão de HRESULT para exceção é que, quando uma API de um componente devolve E_OUTOFMEMORY, isso faz com que seja lançada uma std::bad_alloc.

Prefira winrt::hresult_error::code quando estiver apenas a consultar o código HRESULT. A função winrt::hresult_error::to_abi , por outro lado, converte-se num objeto de erro COM e empurra o estado para o armazenamento local da thread COM.

Lançamento de exceções

Haverá casos em que decidirá que, caso a sua chamada a uma determinada função falhe, a sua aplicação não conseguirá recuperar (já não poderá confiar nela para funcionar de forma previsível). O exemplo de código abaixo usa um valor winrt::handle como invólucro para o HANDLE devolvido por CreateEvent. Depois, passa o identificador (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 seja convertível 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 passa para winrt::check_bool for falso, então ocorre a seguinte sequência de ações.

Como Windows APIs reportam erros em tempo de execução usando vários tipos de valores de retorno, existem, além do winrt::check_bool um punhado de outras funções auxiliares úteis para verificar valores e lançar 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.

Pode usar estas 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.

Lançar exceções ao criar uma API

Todos os limites da Interface Binária de Aplicação do Windows Runtime (ou limites ABI) devem ser noexcept — o que significa que as exceções nunca podem escapar daí. Ao criar uma API, deve sempre marcar o limite da ABI com a palavra-chave C++ noexcept . noexcept tem um comportamento específico em C++. Se uma exceção em C++ atingir um noexcept limite, então o processo falhará rapidamente com std::terminate. Esse comportamento é geralmente desejável, porque uma exceção não tratada quase sempre implica um estado desconhecido no processo.

Como as exceções não devem atravessar o limite da ABI, uma condição de erro que surge numa implementação é devolvida através da camada ABI sob a forma de um código de erro HRESULT. Quando estás a criar uma API usando C++/WinRT, é gerado código para converteres qualquer exceção que incluires na tua implementação num HRESULT. A função winrt::to_hresult é usada nesse código gerado num 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 trata de exceções derivadas de std::exception, e winrt::hresult_error e os seus tipos derivados. Na sua implementação, deve preferir winrt::hresult_error, ou um tipo derivado, para que os consumidores da sua API recebam informações ricas de erro. Std::Exception (que corresponde a E_FAIL) é suportado caso surjam exceções devido ao uso da Biblioteca de Modelos Padrão.

Debuggabilidade com noexcept

Como mencionámos acima, uma exceção em C++ que atinge um noexcept limite falha rapidamente com std::terminate. Isso não é ideal para depuração, porque std::terminate faz frequentemente perder grande parte ou a totalidade da informação sobre o erro ou do contexto da exceção lançada, especialmente quando estão envolvidas corrotinas.

Assim, esta secção trata do caso em que o seu método ABI (que anotou devidamente com noexcept) usa co_await para chamar código assíncrono de projeção de C++/WinRT. Recomendamos que encapsule as chamadas ao código de projeção C++/WinRT num winrt::fire_and_forget. Fazer isso proporciona um local adequado para que uma exceção não tratada seja devidamente registada como uma exceção armazenada, o que aumenta significativamente a depurabilidade.

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 assistente de método incorporado unhandled_exception , que chama winrt::terminate, que por sua vez chama RoFailFastWithErrorContext. Isto garante que qualquer contexto (exceção retida, código de erro, mensagem de erro, rasto da pilha, entre outros) seja preservado, quer para depuração em tempo real quer para um dump de post-mortem. Por conveniência, pode separar a parte “fire-and-forget” numa função à parte que devolve um winrt::fire_and_forget e, em seguida, chamar essa função.

Código síncrono

Em alguns casos, o seu método ABI (que, mais uma vez, anotou devidamente com noexcept) chama apenas código síncrono. Ou seja, nunca usa co_await, nem para chamar um método de Windows Runtime assíncrono, nem para alternar entre threads em primeiro plano e em segundo plano. Nesse caso, a técnica fire_and_forget ainda funciona, mas não é eficiente. Em vez disso, podes fazer algo assim.

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

Falha rapidamente

O código da secção anterior ainda falha rapidamente. Tal como está escrito, esse código não lida com exceções. Qualquer exceção não tratada resulta na terminação do programa.

Mas essa forma é superior, porque garante a facilidade de depuração. Em casos raros, poderá querer try/catch e lidar com determinadas exceções. Mas isso deveria ser raro porque, como este tema explica, desencorajamos o uso de exceções como mecanismo de controlo de fluxo para condições que se esperam.

Lembre-se de que é uma má ideia deixar uma exceção não tratada escapar de um contexto simples noexcept. Nessa condição, o ambiente de execução de C++ irá std::terminate o processo, perdendo, assim, qualquer informação sobre exceções armazenadas que o C++/WinRT registou cuidadosamente.

Assertions

Para suposições internas na sua aplicação, há 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 booleana. WINRT_ASSERT é uma definição macro, e expande-se para _ASSERTE.

WINRT_ASSERT(pos < size());

WINRT_ASSERT é eliminado durante a compilação nas compilações release; numa compilação de depuração, interrompe a aplicação no depurador na linha de código em que a asserção se encontra.

Não deves usar exceções nos teus destruidores. Assim, pelo menos em builds de depuração, podes afirmar o resultado de chamar uma função de um 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