Fehlerbehandlung mit C++/WinRT

In diesem Thema werden Strategien für die Behandlung von Fehlern bei der Programmierung mit C++/WinRT erläutert. Allgemeine Informationen und Hintergründe finden Sie unter "Fehler- und Ausnahmebehandlung (Modern C++)".

Vermeiden des Abfangens und Auslösens von Ausnahmen

Es wird empfohlen, weiterhin ausnahmesicheren Code zu schreiben, sie sollten jedoch möglichst vermeiden, Ausnahmen abzufangen und auszuwerfen. Wenn es keinen Handler für eine Ausnahme gibt, generiert Windows automatisch einen Fehlerbericht (einschließlich eines Minidumps des Absturzes), der Ihnen hilft, nachzuverfolgen, wo sich das Problem befindet.

Lösen Sie keine Ausnahme aus, die erwartungsgemäß abgefangen wird. Verwenden Sie keine Ausnahmen für erwartete Fehler. Lösen Sie eine Ausnahme nur dann aus, wenn ein unerwarteter Laufzeitfehler auftritt, und behandeln Sie alles andere mit Fehler-/Ergebniscodes – direkt und in der Nähe der Fehlerquelle. Auf diese Weise wissen Sie, wenn eine Ausnahme ausgelöst wird , dass die Ursache entweder ein Fehler in Ihrem Code oder ein außergewöhnlicher Fehlerstatus im System ist.

Berücksichtigen Sie das Szenario für den Zugriff auf die Windows-Registrierung. Wenn Ihre App einen Wert aus der Registry nicht lesen kann, ist das zu erwarten, und Sie sollten angemessen damit umgehen. Werfen Sie keine Ausnahme; geben Sie stattdessen einen bool- oder enum-Wert zurück, der angibt, dass der Wert nicht gelesen wurde und möglicherweise auch, warum. Kann ein Wert nicht in die Registrierung geschrieben werden, weist dies wahrscheinlich darauf hin, dass ein größeres Problem vorliegt, als Sie sinnvoll in Ihrer Anwendung behandeln könnten. In einem solchen Fall möchten Sie nicht, dass Ihre Anwendung fortgesetzt wird, daher ist eine Ausnahme, die zu einem Fehlerbericht führt, die schnellste Möglichkeit, Ihre Anwendung daran zu hindern, Schaden zu verursachen.

Ein weiteres Beispiel ist das Abrufen eines Miniaturbilds aus einem Aufruf von StorageFile.GetThumbnailAsync und anschließendes Übergeben dieser Miniaturansicht an BitmapSource.SetSourceAsync. Wenn diese Aufrufsequenz dazu führt, dass Sie nullptr an SetSourceAsync übergeben (die Bilddatei kann nicht gelesen werden; vielleicht lässt ihre Dateierweiterung vermuten, dass sie Bilddaten enthält, tatsächlich ist das aber nicht der Fall), dann wird eine Ausnahme wegen eines ungültigen Zeigers ausgelöst. Wenn Sie in Ihrem Code auf einen solchen Fall stoßen, sollten Sie den Fall nicht als Ausnahme abfangen und behandeln, sondern stattdessen prüfen, ob nullptr von GetThumbnailAsync zurückgegeben wurde.

Das Auslösen von Ausnahmen ist tendenziell langsamer als die Verwendung von Fehlercodes. Wenn Sie eine Ausnahme nur bei schwerwiegenden Fehlern auslösen, geht dies, wenn alles gut läuft, nie zu Lasten der Leistung.

Die Leistung wird eher beeinträchtigt durch den Laufzeit-Overhead, der entsteht, wenn sichergestellt werden muss, dass die richtigen Destruktoren im unwahrscheinlichen Falle aufgerufen werden, dass eine Ausnahme ausgelöst wird. Die Kosten für diese Garantie fallen an, unabhängig davon, ob tatsächlich eine Ausnahme ausgelöst wird oder nicht. Daher sollten Sie sicherstellen, dass der Compiler eine gute Vorstellung davon hat, welche Funktionen möglicherweise Ausnahmen auslösen können. Wenn der Compiler nachweisen kann, dass es keine Ausnahmen von bestimmten Funktionen (der noexcept Spezifikation) gibt, kann er den generierten Code optimieren.

Abfangen von Ausnahmen

Eine Fehlerbedingung, die auf der Windows-Runtime ABI-Ebene auftritt, wird in Form eines HRESULT-Werts zurückgegeben. Sie müssen jedoch keine HRESULTs in Ihrem Code behandeln. Der C++/WinRT-Projektionscode, der für eine API auf der verbrauchenden Seite generiert wird, erkennt einen HRESULT-Fehlercode auf der ABI-Ebene und konvertiert den Code in eine winrt::hresult_error Ausnahme, die Sie abfangen und verarbeiten können. Wenn Sie HRESULTs behandeln möchten, verwenden Sie den winrt::hresult-Typ .

Wenn der Benutzer beispielsweise ein Bild aus der Bildbibliothek löscht, während die Anwendung diese Auflistung durchläuft, löst die Projektion eine Ausnahme aus. Und dies ist ein Fall, in dem Sie diese Ausnahme abfangen und behandeln müssen. Hier ist ein Codebeispiel, das diesen Fall zeigt.

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

Verwenden Sie dasselbe Muster in einer Coroutine beim Aufrufen einer mit co_await markierten Funktion. Ein weiteres Beispiel für diese Konvertierung von HRESULT in eine Ausnahme ist, wenn eine Komponenten-API E_OUTOFMEMORY zurückgibt, wodurch eine std::bad_alloc-Ausnahme ausgelöst wird.

Bevorzugen Sie winrt::hresult_error::code, wenn Sie nur kurz einen Blick auf einen HRESULT-Code werfen. Die winrt::hresult_error::to_abi-Funktion hingegen wandelt in ein COM-Fehlerobjekt um und legt den Zustand im threadspezifischen COM-Speicher ab.

Auslösen von Ausnahmen

Es wird Fälle geben, in denen Sie zu dem Schluss kommen, dass Ihre Anwendung sich nicht davon erholen kann, wenn Ihr Aufruf einer bestimmten Funktion fehlschlägt (Sie können sich dann nicht mehr darauf verlassen, dass sie vorhersehbar funktioniert). Im folgenden Codebeispiel wird ein winrt::handle-Wert als Wrapper für das von CreateEvent zurückgegebene HANDLE verwendet. Anschließend wird der Handle (es wird daraus ein bool-Wert erstellt) an die Funktionsvorlage winrt::check_bool übergeben. winrt::check_bool funktioniert mit einem bool, oder mit einem beliebigen Wert, der in false (eine Fehlerbedingung) oder true (eine Erfolgsbedingung) konvertiert werden kann.

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

Wenn der Wert, den Sie an winrt::check_bool übergeben, "false" ist, werden die folgenden Aktionen ausgeführt.

Da Windows APIs Laufzeitfehler mithilfe verschiedener Rückgabewerttypen melden, gibt es zusätzlich zu winrt::check_bool eine Handvoll anderer nützlicher Hilfsfunktionen zum Überprüfen von Werten und Auslösen von Ausnahmen.

  • winrt::check_hresult. Überprüft, ob der HRESULT-Code einen Fehler darstellt, und ruft in diesem Falls winrt::throw_hresult auf.
  • winrt::check_nt. Überprüft, ob ein Code einen Fehler darstellt, und ruft in diesem Falls ja winrt::throw_hresult auf.
  • winrt::check_pointer. Überprüft, ob ein Zeiger null ist, und ruft in diesem Fall winrt::throw_last_error auf.
  • winrt::check_win32. Überprüft, ob ein Code einen Fehler darstellt, und ruft in diesem Falls ja winrt::throw_hresult auf.

Sie können diese Hilfsfunktionen für allgemeine Rückgabecodetypen verwenden oder auf eine beliebige Fehlerbedingung reagieren und winrt::throw_last_error oder winrt::throw_hresult aufrufen.

Auslösen von Ausnahmen beim Erstellen einer API

Alle Windows-Runtime Application Binary Interface-Begrenzungen (oder ABI-Begrenzungen) dürfen keine Ausnahme sein, was bedeutet, dass Ausnahmen dort niemals escapen dürfen. Wenn Sie eine API erstellen, sollten Sie immer die ABI-Grenze mit dem C++ noexcept -Schlüsselwort markieren. noexcept weist ein bestimmtes Verhalten in C++ auf. Wenn eine C++-Ausnahme eine noexcept-Grenze überschreitet, wird der Prozess sofort mit std::terminate beendet. Dieses Verhalten ist im Allgemeinen wünschenswert, da eine unbehandelte Ausnahme fast immer unbekannten Zustand im Prozess impliziert.

Da Ausnahmen die ABI-Grenze nicht überschreiten dürfen, wird eine Fehlerbedingung, die in einer Implementierung entsteht, über die ABI-Ebene in Form eines HRESULT-Fehlercodes zurückgegeben. Wenn Sie eine API mit C++/WinRT erstellen, wird Code generiert, um eine Ausnahme zu konvertieren, die Sie in Ihrer Implementierung in ein HRESULT auslösen. Die Winrt::to_hresult-Funktion wird in diesem generierten Code in einem Muster wie diesem verwendet.

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 behandelt ausnahmen, die von "std::exception" und "winrt::hresult_error " und den abgeleiteten Typen abgeleitet werden. In Ihrer Implementierung sollten Sie "winrt::hresult_error" oder einen abgeleiteten Typ bevorzugen, damit Verbraucher Ihrer API umfangreiche Fehlerinformationen erhalten. std::exception (das E_FAIL zugeordnet wird) wird unterstützt, wenn bei der Verwendung der Standard Template Library Ausnahmen auftreten.

Debuggbarkeit mit noexcept

Wie bereits erwähnt, schlägt eine C++-Ausnahme, die auf eine noexcept Grenze trifft, schnell mit std::terminate fehl. Das ist für das Debuggen nicht ideal, weil std::terminate häufig sehr viel oder den gesamten ausgegebenen Fehler- oder Ausnahmekontext verliert, insbesondere dann, wenn Coroutinen beteiligt sind.

Also befasst sich dieser Abschnitt mit dem Fall, dass Ihre ABI-Methode, die Sie ordnungsgemäß mit noexcept annotiert haben, co_await verwendet, um asynchronen C++/WinRT-Projektionscode aufzurufen. Wir empfehlen, die Aufrufe an den C++/WinRT-Projektionscode in winrt::fire_and_forget zu kapseln. Auf diese Weise erfolgt eine ordnungsgemäße Erfassung von Ausnahmefehlern als Stowed-Ausnahme, wodurch die Debuggingfähigkeit beträchtlich verbessert wird.

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 verfügt über ein integriertes unhandled_exception Methodenhilfsprogramm, das winrt::terminate aufruft, wodurch wiederum RoFailFastWithErrorContext aufgerufen wird. Dadurch wird sichergestellt, dass sämtliche Kontextinformationen (stowed exception, Fehlercode, Fehlermeldung, Stack-Backtrace usw.) entweder für das Live-Debugging oder für einen Post-Mortem-Dump erhalten bleiben. Aus praktischen Gründen kannst du den fire-and-forget-Teil in eine separate Funktion einbeziehen, die winrt::fire_and_forget zurückgibt und anschließend aufruft.

Synchroner Code

In einigen Fällen ruft Ihre ABI-Methode (die Sie erneut mit Anmerkungen noexceptversehen haben) nur synchronen Code auf. Mit anderen Worten: Es verwendet co_await niemals, weder zum Aufrufen einer asynchronen Windows-Runtime-Methode noch zum Wechseln zwischen Vordergrund- und Hintergrundthreads. In diesem Fall funktioniert die fire_and_forget-Technik zwar noch, ist aber nicht effizient. Stattdessen können Sie etwa so vorgehen.

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

Fail-Fast

Der Code im vorherigen Abschnitt schlägt immer noch schnell fehl. Wie geschrieben, behandelt dieser Code keine Ausnahmen. Jede unbehandelte Ausnahme führt zum Beenden des Programms.

Diese Form ist jedoch überlegen, da sie die Debugbarkeit gewährleistet. In seltenen Fällen musst du try/catch verwenden, um bestimmte Ausnahmen zu behandeln. Das sollte jedoch selten sein, denn wie in diesem Thema erläutert, wird davon abgeraten, Ausnahmen als Ablaufsteuerungsmechanismus für bedingungen zu verwenden, die Sie erwarten.

Denken Sie daran, dass es keine gute Idee ist, zuzulassen, dass eine unbehandelte Ausnahme einem nackten noexcept-Kontext entweicht. Unter dieser Bedingung wird die C++-Laufzeit den Prozess mit std::terminate beenden, wodurch alle gespeicherten Ausnahmeinformationen verloren gehen, die C++/WinRT sorgfältig gesichert hat.

Assertions

Für interne Annahmen in Ihrer Anwendung gibt es Assertionen. Bevorzugen Sie static_assert für die Kompilierungszeitüberprüfung, wo immer möglich. Verwenden Sie für Laufzeitbedingungen WINRT_ASSERT mit einem booleschen Ausdruck. WINRT_ASSERT ist eine Makrodefinition und wird auf _ASSERTE erweitert.

WINRT_ASSERT(pos < size());

WINRT_ASSERT wird bei Releasebuilds kompiliert. In einem Debugbuild beendet WINRT_ASSERT die Anwendung im Debugger in der Codezeile, in der sich die Assertion befindet.

Sie sollten keine Ausnahmen in Ihren Destruktoren verwenden. Folglich können Sie zumindest in Debugbuilds das Ergebnis des Aufrufs einer Funktion von einem Destruktor mit WINRT_VERIFY (mit einem booleschen Ausdruck) und WINRT_VERIFY_ (mit dem erwarteten Ergebnis und einem booleschen Ausdruck) bestätigen.

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

Wichtige APIs