Zpracování chyb pomocí C++/WinRT

Toto téma popisuje strategie pro zpracování chyb při programování pomocí C++/WinRT. Obecnější informace a pozadí najdete v tématu Chyby a zpracování výjimek (moderní jazyk C++).

Vyhněte se zachytávání a vyvolávání výjimek

Doporučujeme pokračovat v psaní kódu bezpečného pro výjimky, ale dáváte přednost tomu, abyste se vyhnuli zachycení a vyvolání výjimek, kdykoli je to možné. Pokud pro výjimku neexistuje žádná obslužná rutina, Windows automaticky vygeneruje hlášení o chybě (včetně minidumpu pádu), které vám pomůže zjistit, kde problém nastal.

Nevyvolávejte výjimku, u které očekáváte, že ji zachytíte. A nepoužívejte výjimky pro očekávané chyby. Vyvolejte výjimku pouze tehdy, když dojde k neočekávané chybě za běhu, a vše ostatní zpracovávejte pomocí kódů chyb/výsledků – přímo a co nejblíže zdroji selhání. Když dojde k vyvolání výjimky, víte, že příčinou je chyba v kódu nebo výjimečný chybový stav v systému.

Zvažte scénář přístupu k registru Windows. Pokud se vaší aplikaci nepodaří přečíst hodnotu z registru, je potřeba ji očekávat a měli byste ji správně zpracovat. Nevyvolávejte výjimku; raději vraťte hodnotu bool nebo enum, která udává, že se hodnotu nepodařilo načíst, a případně i proč. Pokud se naopak nepodaří zapsat hodnotu do registru, pravděpodobně to znamená, že jde o závažnější problém, než který lze ve vaší aplikaci rozumně ošetřit. V takovém případě nechcete, aby vaše aplikace pokračovala, takže výjimka, která vede k hlášení chyb, je nejrychlejší způsob, jak zabránit vaší aplikaci v tom, aby způsobila případné škody.

V jiném příkladu zvažte načtení miniatury z volání StorageFile.GetThumbnailAsync a předání této miniatury BitmapSource.SetSourceAsync. Pokud tato posloupnost volání způsobí předání nullptr do SetSourceAsync (soubor obrázku se nedá přečíst; jeho přípona souboru může vypadat, jako by obsahovala data obrázku, ale ve skutečnosti ne), způsobíte vyvolání neplatné výjimky ukazatele. Pokud v kódu narazíte na takový případ, místo jeho zachytávání a zpracování jako výjimky raději zkontrolujte hodnotu nullptr, kterou vrací metoda GetThumbnailAsync.

Vyvolání výjimek je pomalejší než použití kódů chyb. Pokud vyvoláte výjimku pouze v případě závažné chyby, pak pokud vše půjde dobře, nikdy nezaplatíte cenu za výkon.

Pravděpodobnější dopad na výkon však souvisí s režií za běhu programu spojenou se zajištěním volání příslušných destruktorů v nepravděpodobném případě, že je vyvolána výjimka. Náklady na tuto záruku přicházejí bez ohledu na to, jestli dojde k výjimce, nebo ne. Proto byste měli zajistit, aby kompilátor měl dobrou představu o tom, jaké funkce můžou potenciálně vyvolat výjimky. Pokud kompilátor dokáže prokázat, že nedojde k žádným výjimkám z určitých funkcí ( noexcept specifikace), může optimalizovat kód, který vygeneruje.

Zachycení výjimek

Chybový stav, který vzniká na vrstvě prostředí Windows Runtime ABI, je vrácen ve formě hodnoty HRESULT. V kódu ale nemusíte zpracovávat HRESULT. Kód projekce C++/WinRT vygenerovaný pro rozhraní API na straně klienta detekuje ve vrstvě ABI chybový kód HRESULT a převede ho na výjimku winrt::hresult_error, kterou můžete zachytit a zpracovat. Pokud chcete zpracovat hodnoty HRESULT, použijte typ winrt::hresult .

Pokud se například stane, že uživatel odstraní obrázek z knihovny obrázků, zatímco vaše aplikace iteruje tuto kolekci, vyvolá projekce výjimku. A to je případ, kdy budete muset zachytit a zpracovat tuto výjimku. Tady je příklad kódu, který ukazuje tento případ.

#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.
        }
    }
}

V korutině použijte při volání funkce s co_await stejný vzor. Dalším příkladem tohoto převodu z HRESULT na výjimku je situace, kdy rozhraní API komponenty vrátí E_OUTOFMEMORY, což způsobí vyvolání výjimky std::bad_alloc.

Upřednostněte winrt::hresult_error::code, když se jen díváte na kód HRESULT. Funkce winrt::hresult_error::to_abi naopak převádí na chybový objekt COM a ukládá stav do místního úložiště vláken modelu COM.

Vyvolání výjimek

V případech, kdy se rozhodnete, že pokud vaše volání dané funkce selže, vaše aplikace se nebude moct zotavit (už se na ni nebudete moct spolehnout, aby fungovala předvídatelně). Následující příklad kódu používá hodnotu winrt::handle jako obálku kolem handle vráceného z CreateEvent. Potom předá popisovač (z něj vytvoří hodnotu bool) funkční šabloně winrt::check_bool. winrt::check_bool funguje s bool, nebo s jakoukoli hodnotou, kterou lze převést na false (chybový stav) nebo true (úspěšný stav).

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

Pokud je hodnota, kterou předáte winrt::check_bool false, provede se následující posloupnost akcí.

Vzhledem k tomu, že rozhraní API Windows hlásí chyby za běhu pomocí různých typů návratových hodnot, existují kromě winrt::check_bool několika dalších užitečných pomocných funkcí pro kontrolu hodnot a vyvolání výjimek.

  • winrt::check_hresult. Zkontroluje, zda kód HRESULT představuje chybu, a pokud ano, volá winrt::throw_hresult.
  • winrt::check_nt. Zkontroluje, jestli kód představuje chybu, a pokud ano, volá winrt::throw_hresult.
  • winrt::check_pointer. Zkontroluje, zda je ukazatel null, a pokud ano, volá winrt::throw_last_error.
  • winrt::check_win32. Zkontroluje, jestli kód představuje chybu, a pokud ano, volá winrt::throw_hresult.

Tyto pomocné funkce můžete použít pro běžné typy návratových kódů nebo můžete reagovat na jakýkoli chybový stav a volat winrt::throw_last_error nebo winrt::throw_hresult.

Vyvolání výjimek při vytváření rozhraní API

Všechny hranice binárního rozhraní aplikace prostředí Windows Runtime (nebo hranice ABI) musí být noexcept—to znamená, že se odtud výjimky nikdy nesmí šířit. Při vytváření rozhraní API byste měli vždy označit hranici ABI klíčovým slovem C++ noexcept . noexcept má specifické chování v jazyce C++. Pokud výjimka C++ narazí na hranici noexcept, proces bude okamžitě ukončen voláním std::terminate. Toto chování je obecně žádoucí, protože neošetřená výjimka téměř vždy značí neznámý stav procesu.

Vzhledem k tomu, že výjimky nesmí překročit hranici ABI, je vrácena chybová podmínka, která vzniká v implementaci ve vrstvě ABI ve formě kódu chyby HRESULT. Při vytváření rozhraní API pomocí C++/WinRT se vygeneruje kód, který převede všechny výjimky, které v implementaci vyvoláte , na HRESULT. Funkce winrt::to_hresult se používá v tomto vygenerovaném kódu v podobném vzoru.

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 zpracovává výjimky odvozené z std::exception a winrt::hresult_error a jeho odvozených typů. V implementaci byste měli preferovat winrt::hresult_error nebo odvozený typ, aby příjemci vašeho rozhraní API dostávali bohaté informace o chybách. Std::exception (která se mapuje na E_FAIL) je podporována v případě, že výjimky vznikají z vašeho použití standardní knihovny šablon.

Možnost ladění s noexcept

Jak jsme zmínili výše, výjimka v jazyce C++, která narazí na hranici noexcept, okamžitě skončí voláním std::terminate. To není pro ladění ideální, protože při std::terminate se často ztratí velká část nebo veškeré informace o chybě či kontext vyvolané výjimky, zejména jsou-li použity korutiny.

Tato část se tedy zabývá případem, kdy vaše metoda ABI (kterou jste správně anotovali noexceptpomocí) používá co_await k volání asynchronního kódu projekce C++/WinRT. Doporučujeme, abyste volání projekčního kódu C++/WinRT obalili do winrt::fire_and_forget. Tím se vytvoří vhodné místo, kde lze neošetřenou výjimku řádně zaznamenat jako stowed exception, což výrazně usnadňuje ladění.

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 má předdefinovaný unhandled_exception pomocník metody, který volá winrt::terminate, který pak volá RoFailFastWithErrorContext. To zaručuje, že veškerý kontext (odložená výjimka, kód chyby, chybová zpráva, zpětné trasování zásobníku atd.) je zachován buď pro živé ladění, nebo pro výpis paměti po selhání. Pro usnadnění můžete část typu fire-and-forget vyčlenit do samostatné funkce, která vrací winrt::fire_and_forget, a tu pak zavolat.

Synchronní kód

V některých případech vaše metoda ABI (kterou jste, opět, správně anotovali pomocí noexcept) volá pouze synchronní kód. Jinými slovy, co_await nikdy nepoužívá ani k volání asynchronní metody rozhraní prostředí Windows Runtime, ani k přepínání mezi vlákny popředí a pozadí. V takovém případě bude technika „fire_and_forget“ i nadále fungovat, ale není příliš efektivní. Místo toho můžete něco takového udělat.

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

Rychlé selhání

Kód v předchozí části stále selže okamžitě. Jak je uvedeno, tento kód nezpracuje žádné výjimky. Jakékoli neošetřené výjimky způsobí ukončení programu.

Tato podoba je ale lepší, protože umožňuje ladění. Ve výjimečných případech můžete chtít try/catcha zpracovat určité výjimky. To by ale mělo být vzácné, protože vzhledem k tomu, že toto téma vysvětluje, nedoporučujeme používat výjimky jako mechanismus řízení toku pro očekávané podmínky.

Pamatujte, že není dobrý nápad nechat z holého kontextu noexcept uniknout neošetřenou výjimku. Za těchto podmínek modul runtime jazyka C++ ukončí proces voláním std::terminate, čímž se ztratí veškeré informace o odložené výjimce, které C++/WinRT pečlivě zaznamenal.

Assertions

Pro interní předpoklady ve vaší aplikaci slouží asserty. Pokud je to možné, preferujte static_assert pro ověřování v době kompilace. Pro podmínky za běhu programu použijte WINRT_ASSERT s booleovským výrazem. WINRT_ASSERT je definice makra a rozbalí se na _ASSERTE.

WINRT_ASSERT(pos < size());

WINRT_ASSERT je zkompilován v buildech vydaných verzí; v sestavení ladění zastaví aplikaci v ladicím programu na řádku kódu, kde je kontrolní výraz.

V destruktoorech byste neměli používat výjimky. Takže alespoň v ladicích sestaveních můžete v destruktoru ověřit výsledek volání funkce pomocí WINRT_VERIFY (s logickým výrazem) a WINRT_VERIFY_ (s očekávaným výsledkem a logickým výrazem).

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

Důležitá rozhraní API