Gestione degli errori con C++/WinRT

Questo argomento illustra le strategie per la gestione degli errori durante la programmazione con C++/WinRT. Per informazioni più generali e più contesto, vedi Gestione degli errori e delle eccezioni (Modern C++).

Evitare di intercettare e generare eccezioni

Ti consigliamo di continuare a scrivere codice a prova di eccezioni e di evitare di intercettare e generare eccezioni ogni volta che è possibile. Se non è presente un gestore per un'eccezione, Windows genera automaticamente una segnalazione errori (incluso un minidump dell'arresto anomalo), che consente di individuare il problema.

Non generare un'eccezione che prevedi di intercettare. Non usare eccezioni per gli errori previsti. Genera un'eccezione solo quando si verifica un errore di runtime imprevisto e gestisci tutto il resto con codici di errore/ risultato, direttamente e vicino all'origine dell'errore. In questo modo, quando viene effettivamente generata un'eccezione, saprai che la causa è un bug nel codice o uno stato di errore eccezionale del sistema.

Considera uno scenario di accesso al Registro di sistema di Windows. È prevedibile che l'app non riesca a leggere un valore dal Registro di sistema e questa evenienza va gestita normalmente. Non generare un'eccezione. Restituisci invece un valore bool o enum che indica che il valore non è stato letto e forse la motivazione. La mancata scrittura di un valore nel Registro di sistema, invece, indica probabilmente un problema più grave che puoi gestire in modo intelligente nell'applicazione. In un caso simile, l'applicazione non deve continuare. Un'eccezione che produce una segnalazione errori è quindi il modo più rapido per impedire all'applicazione di causare danni.

Per un altro esempio, considera il recupero di un'immagine di anteprima da una chiamata a StorageFile.GetThumbnailAsync e il passaggio dell'anteprima a BitmapSource.SetSourceAsync. Se tale sequenza di chiamate provoca il passaggio di nullptr a SetSourceAsync (non è possibile leggere il file di immagine, ad esempio perché in base all'estensione del file sembra erroneamente che contenga dati di immagine), verrà generata un'eccezione per puntatore non valido. Se trovi un caso simile nel tuo codice, anziché intercettare e gestire il caso come un'eccezione, cerca nullptr restituito da GetThumbnailAsync.

La generazione di eccezioni è tendenzialmente più lenta rispetto all'uso di codici errore. Se viene generata un'eccezione solo quando si verifica un errore irreversibile, se tutto va bene le prestazioni non verranno mai compromesse.

Per garantire meglio le prestazioni, occorre però fare in modo che, nell'improbabile caso in cui venga generata un'eccezione, vengano chiamati i distruttori appropriati. Il prezzo di questa garanzia si paga sia che venga effettivamente generata un'eccezione oppure no. Assicurati quindi che il compilatore conosca bene le funzioni che potrebbero generare eccezioni. Se il compilatore può dimostrare che determinate funzioni non genereranno eccezioni (specifica noexcept), può ottimizzare il codice generato.

Intercettazione di eccezioni

Una condizione di errore che si verifica al livello ABI di Windows Runtime viene restituita sotto forma di valore HRESULT. Ma non è necessario gestire gli HRESULT nel codice. Il codice di proiezione C++/WinRT generato per un'API sul lato consumer rileva un codice di errore HRESULT nel livello ABI e converte il codice in un'eccezione winrt::hresult_error, che puoi intercettare e gestire. Se vuoi gestire i valori HRESULT, usa il tipo winrt::hresult.

Ad esempio, se l'utente elimina per caso un'immagine dalla Raccolta di immagini mentre l'applicazione esegue un'iterazione su tale raccolta, la proiezione genera un'eccezione. Questo è un caso in cui è necessario intercettare e gestire l'eccezione. Ecco un esempio di codice che illustra questo 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.
        }
    }
}

Usa questo stesso modello in una coroutine quando chiami una funzione con co_await. Un altro esempio di questa conversione da HRESULT in eccezione è quando un'API di componente restituisce E_OUTOFMEMORY, causando la generazione di un'eccezione std::bad_alloc.

Accordare la preferenza a winrt::hresult_error::code quando si sta semplicemente visualizzando in anteprima codice HRESULT. La funzione winrt::hresult_error::to_abi viene invece convertita in un oggetto di errore COM ed esegue il push dello stato nell'archiviazione locale del thread COM.

Generazione di eccezioni

In alcuni casi potrai decidere che, se la chiamata a una determinata funzione non riesce, l'applicazione non sarà in grado di recuperare e non potrai più fare affidamento su un funzionamento prevedibile. L'esempio di codice seguente usa un valore winrt::handle come wrapper per l'HANDLE restituito da CreateEvent. Passa quindi l'handle (creando un valore bool da quest'ultimo) al modello di funzione winrt::check_bool. winrt::check_bool funziona con un valore bool o con qualsiasi valore convertibile in false (condizione di errore) o in true (condizione di riuscita).

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

Se il valore che viene passato a winrt::check_bool è false, viene eseguita questa sequenza di azioni.

  • winrt::check_bool chiama la funzione winrt::throw_last_error.
  • winrt::throw_last_error chiama GetLastError per recuperare il valore dell'ultimo codice di errore del thread di chiamata e quindi chiama la funzione winrt::throw_hresult.
  • winrt::throw_hresult genera un'eccezione usando un oggetto winrt::hresult_error (o un oggetto standard) che rappresenta tale codice di errore.

Poiché le API di Windows segnalano errori di runtime con vari tipi di valori restituiti, oltre a winrt::check_bool esiste un numero limitato di altre funzioni helper utili per controllare i valori e generare eccezioni.

  • winrt::check_hresult. Controlla se il codice HRESULT rappresenta un errore e, in caso affermativo, chiama winrt::throw_hresult.
  • winrt::check_nt. Controlla se un codice rappresenta un errore e, in caso affermativo, chiama winrt::throw_hresult.
  • winrt::check_pointer. Controlla se un puntatore è Null e, in caso affermativo, chiama winrt::throw_last_error.
  • winrt::check_win32. Controlla se un codice rappresenta un errore e, in caso affermativo, chiama winrt::throw_hresult.

Puoi usare queste funzioni helper per i tipi comuni di codice restituito oppure puoi rispondere a qualsiasi condizione di errore e chiamare winrt::throw_last_error o winrt::throw_hresult.

Generazione di eccezioni durante la creazione di un'API

Tutti i limiti dell'interfaccia binaria dell'applicazione di Windows Runtime (o limiti ABI) devono essere noexcept. In altre parole, le eccezioni non devono mai superare tali limiti. Quando crei un'API, devi sempre contrassegnare il limite ABI con la parola chiave noexcept di C++. noexcept ha un comportamento specifico in C++. Se un'eccezione C++ raggiunge un limite noexcept, il processo verrà terminato rapidamente con std::terminate. Questo comportamento è in genere auspicabile, perché un'eccezione non gestita implica quasi sempre uno stato sconosciuto nel processo.

Poiché le eccezioni non possono superare il limite ABI, un'eventuale condizione di errore di un'implementazione viene restituita oltre il livello ABI sotto forma di codice di errore HRESULT. Quando crei un'API con C++/WinRT, il codice viene generato per permetterti di convertire in HRESULT qualsiasi eccezione effettivamente generata nella tua implementazione. La funzione winrt::to_hresult viene usata nel codice generato in un modello simile al seguente.

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 gestisce le eccezioni derivate da std::exception, da winrt::hresult_error e dai relativi tipi derivati. Nell'implementazione usa di preferenza winrt::hresult_error, o un tipo derivato, in modo che i consumer dell'API ricevano informazioni dettagliate sull'errore. std::exception, che esegue il mapping a E_FAIL, è supportata nel caso in cui le eccezioni derivano dall'uso della raccolta di modelli standard.

Possibilità di debug con noexcept

Come indicato in precedenza, un'eccezione C++ che raggiunge un limite noexcept ha l'effetto di terminare rapidamente il processo con std::terminate. Questa condizione non è l'ideale per il debug perché std::terminate spesso perde gran parte o tutto il contesto generato per l'errore o l'eccezione, soprattutto quando sono presenti coroutine.

Questa sezione tratta quindi il caso in cui il metodo ABI, che hai opportunamente annotato con noexcept, usa co_await per chiamare il codice di proiezione C++/WinRT asincrono. Ti consigliamo di eseguire il wrapping delle chiamate nel codice di proiezione C++/WinRT all'interno di un'istanza di winrt::fire_and_forget. In questo modo viene fornita una posizione appropriata per registrare correttamente un'eccezione non gestita come eccezione di tipo stowed, aumentando così notevolmente le possibilità di debug.

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 ha un metodo helper unhandled_exception incorporato che chiama winrt::terminate, il quale a sua volta chiama RoFailFastWithErrorContext. In questo modo viene garantita la conservazione di qualsiasi contesto (eccezione di tipo stowed, codice di errore, messaggio di errore, backtrace dello stack e così via) per il debug in tempo reale o per un dump post-mortem. Per praticità, puoi eseguire il factoring della parte fire-and-forget in una funzione separata che restituisce un'istanza di winrt::fire_and_forget e quindi chiamare tale funzione.

Codice sincrono

In alcuni casi, il metodo ABI (anche questa volta opportunamente annotato con noexcept) chiama solo il codice sincrono. In altre parole, non usa mai co_await, né per chiamare un metodo di Windows Runtime asincrono né per passare dai thread in primo piano a quelli in background e viceversa. In tal caso, la tecnica fire_and_forget continua a funzionare, ma non è efficiente. In alternativa, puoi eseguire un'operazione simile alla seguente.

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

Terminazione rapida

Il codice nella sezione precedente termina ancora rapidamente. Così com'è scritto, il codice non gestisce alcuna eccezione. Le eventuali eccezioni non gestite hanno come risultato la terminazione del programma.

Questa forma è tuttavia più efficace perché garantisce la possibilità di eseguire il debug. In rari casi può essere opportuno eseguire try/catch e gestire determinate eccezioni. Tuttavia, questa soluzione dovrebbe essere adottata raramente perché, come illustrato in questo argomento, è sconsigliabile usare le eccezioni come meccanismo di controllo del flusso per condizioni previste.

Tieni presente che non è una buona idea consentire a un'eccezione non gestita di sfuggire da un contesto noexcept di tipo naked. In tale condizione, il runtime C++ applicherà std::terminate al processo, perdendo così le informazioni relative alle eccezioni di tipo stowed accuratamente registrate da C++/WinRT.

Asserzioni

Per i presupposti interni all'applicazione, sono disponibili le asserzioni. Quando possibile, preferisci static_assert per la convalida in fase di compilazione. Per le condizioni della fase di runtime, usa WINRT_ASSERT con un'espressione booleana. WINRT_ASSERT è una definizione di macro e si espande in ASSERTE.

WINRT_ASSERT(pos < size());

WINRT_ASSERT viene compilata nelle build di versione. In una build di debug arresta l'applicazione nel debugger in corrispondenza della riga di codice in cui si trova l'asserzione.

Non usare le eccezioni nei tuoi distruttori. In questo modo, almeno nelle build di debug puoi eseguire un'asserzione del risultato della chiamata a una funzione da un distruttore con WINRT_VERIFY (con un'espressione booleana) e WINRT_VERIFY_ (con un risultato previsto e un'espressione booleana).

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

API importanti