Fehlerbehandlung in COM (Erste Schritte mit Win32 und C++)

COM verwendet HRESULT-Werte , um den Erfolg oder Fehler eines Methoden- oder Funktionsaufrufs anzugeben. Verschiedene SDK-Header definieren verschiedene HRESULT-Konstanten . In WinError.h wird ein allgemeiner Satz systemweiter Codes definiert. In der folgenden Tabelle sind einige dieser systemweiten Rückgabecodes aufgeführt.

Konstant Numerischer Wert BESCHREIBUNG
E_ACCESSDENIED 0x80070005 Zugriff verweigert.
E_FAIL 0x80004005 Unbekannter Fehler.
E_INVALIDARG 0x80070057 Ungültiger Parameterwert.
E_OUTOFMEMORY 0x8007000E Nicht genügend Arbeitsspeicher.
E_POINTER 0x80004003 NULL wurde für einen Zeigerwert falsch übergeben.
E_UNEXPECTED 0x8000FFFF Unerwartete Bedingung.
S_OK 0x0 Erfolg.
S_FALSE 0x1 Erfolg.

 

Alle Konstanten mit dem Präfix "E_" sind Fehlercodes. Die Konstanten S_OK und S_FALSE sind beide Erfolgscodes. Wahrscheinlich geben 99 % der COM-Methoden S_OK zurück, wenn sie erfolgreich sind; aber lassen Sie sich von dieser Tatsache nicht irreführen. Eine Methode gibt möglicherweise andere Erfolgscodes zurück. Testen Sie daher immer mithilfe des Makros SUCCEEDED oder FAILED auf Fehler. Der folgende Beispielcode zeigt die falsche Und die richtige Methode, um den Erfolg eines Funktionsaufrufs zu testen.

// Wrong.
HRESULT hr = SomeFunction();
if (hr != S_OK)
{
    printf("Error!\n"); // Bad. hr might be another success code.
}

// Right.
HRESULT hr = SomeFunction();
if (FAILED(hr))
{
    printf("Error!\n"); 
}

Der Erfolgscode S_FALSE verdient Erwähnung. Einige Methoden verwenden S_FALSE , um ungefähr eine negative Bedingung zu bedeuten, die kein Fehler ist. Es kann auch auf ein "no-op" hindeuten– die Methode war erfolgreich, hatte aber keine Auswirkung. Beispielsweise gibt die CoInitializeEx-FunktionS_FALSE zurück, wenn Sie sie ein zweites Mal aus demselben Thread aufrufen. Wenn Sie zwischen S_OK und S_FALSE in Ihrem Code unterscheiden müssen, sollten Sie den Wert direkt testen, aber dennoch FAILED oder SUCCEEDED verwenden, um die verbleibenden Fälle zu behandeln, wie im folgenden Beispielcode gezeigt.

if (hr == S_FALSE)
{
    // Handle special case.
}
else if (SUCCEEDED(hr))
{
    // Handle general success case.
}
else
{
    // Handle errors.
    printf("Error!\n"); 
}

Einige HRESULT-Werte gelten spezifisch für ein bestimmtes Feature oder Subsystem von Windows. Beispielsweise definiert die Direct2D-Grafik-API den Fehlercode D2DERR_UNSUPPORTED_PIXEL_FORMAT. Dies bedeutet, dass das Programm ein nicht unterstütztes Pixelformat verwendet hat. Die MSDN-Dokumentation enthält häufig eine Liste mit bestimmten Fehlercodes, die von einer Methode zurückgegeben werden können. Sie sollten diese Listen jedoch nicht als endgültig betrachten. Eine Methode kann immer einen HRESULT-Wert zurückgeben, der nicht in der Dokumentation aufgeführt ist. Verwenden Sie erneut die Makros SUCCEEDED und FAILED . Wenn Sie auf einen bestimmten Fehlercode testen, schließen Sie auch einen Standardfall ein.

if (hr == D2DERR_UNSUPPORTED_PIXEL_FORMAT)
{
    // Handle the specific case of an unsupported pixel format.
}
else if (FAILED(hr))
{
    // Handle other errors.
}

Muster für die Fehlerbehandlung

In diesem Abschnitt werden einige Muster für die strukturierte Behandlung von COM-Fehlern behandelt. Jedes Muster hat Vor- und Nachteile. Bis zu einem gewissen Grad ist die Wahl Geschmackssache. Wenn Sie an einem vorhandenen Projekt arbeiten, verfügt es möglicherweise bereits über Programmierrichtlinien, die einen bestimmten Stil verbieten. Unabhängig davon, welches Muster Sie annehmen, befolgt robuster Code die folgenden Regeln.

  • Überprüfen Sie für jede Methode oder Funktion, die ein HRESULT zurückgibt, den Rückgabewert, bevor Sie fortfahren.
  • Geben Sie Ressourcen frei, nachdem sie verwendet wurden.
  • Versuchen Sie nicht, auf ungültige oder nicht initialisierte Ressourcen wie NULL-Zeiger zuzugreifen.
  • Versuchen Sie nicht, eine Ressource zu verwenden, nachdem Sie sie freigegeben haben.

Unter Berücksichtigung dieser Regeln finden Sie hier vier Muster für die Behandlung von Fehlern.

Geschachtelte Ifs

Verwenden Sie nach jedem Aufruf, der ein HRESULT zurückgibt, eine if-Anweisung , um den Erfolg zu testen. Fügen Sie dann den nächsten Methodenaufruf in den Bereich der if-Anweisung ein. Weitere if-Anweisungen können so tief wie nötig geschachtelt werden. In den vorherigen Codebeispielen in diesem Modul wurde dieses Muster verwendet, aber hier ist es wieder:

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->Show(NULL);
        if (SUCCEEDED(hr))
        {
            IShellItem *pItem;
            hr = pFileOpen->GetResult(&pItem);
            if (SUCCEEDED(hr))
            {
                // Use pItem (not shown). 
                pItem->Release();
            }
        }
        pFileOpen->Release();
    }
    return hr;
}

Vorteile

  • Variablen können mit minimalem Umfang deklariert werden. Beispielsweise wird pItem erst deklariert, wenn es verwendet wird.
  • Innerhalb jeder if-Anweisung sind bestimmte Invarianten true: Alle vorherigen Aufrufe sind erfolgreich, und alle abgerufenen Ressourcen sind weiterhin gültig. Wenn das Programm im vorherigen Beispiel die innerste if-Anweisung erreicht, sind sowohl pItem als auch pFileOpen als gültig bekannt.
  • Es ist klar, wann Schnittstellenzeiger und andere Ressourcen freigegeben werden. Sie geben am Ende der if-Anweisung eine Ressource frei, die unmittelbar auf den Aufruf folgt, der die Ressource abgerufen hat.

Nachteile

  • Manche Leute finden tiefe Schachtelungen schwer zu lesen.
  • Die Fehlerbehandlung wird mit anderen Verzweigungs- und Schleifenanweisungen gemischt. Dies kann dazu führen, dass die Gesamteinführung der Programmlogik schwieriger ist.

Kaskadierende Ifs

Verwenden Sie nach jedem Methodenaufruf eine if-Anweisung , um den Erfolg zu testen. Wenn die Methode erfolgreich ist, platzieren Sie den nächsten Methodenaufruf im if-Block . Anstatt jedoch weitere If-Anweisungen zu verschachteln, platzieren Sie jeden nachfolgenden SUCCEEDED-Test nach dem vorherigen if-Block . Wenn eine Methode fehlschlägt, schlagen alle verbleibenden SUCCEEDED-Tests einfach fehl, bis das Ende der Funktion erreicht ist.

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen = NULL;
    IShellItem *pItem = NULL;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));

    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->Show(NULL);
    }
    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->GetResult(&pItem);
    }
    if (SUCCEEDED(hr))
    {
        // Use pItem (not shown).
    }

    // Clean up.
    SafeRelease(&pItem);
    SafeRelease(&pFileOpen);
    return hr;
}

Bei diesem Muster geben Sie Ressourcen ganz am Ende der Funktion frei. Wenn ein Fehler auftritt, sind einige Zeiger möglicherweise ungültig, wenn die Funktion beendet wird. Das Aufrufen von Release für einen ungültigen Zeiger stürzt das Programm (oder schlimmer) ab, sodass Sie alle Zeiger auf NULL initialisieren und überprüfen müssen, ob sie NULL sind, bevor Sie sie freigeben. In diesem Beispiel wird die SafeRelease -Funktion verwendet. Intelligente Zeiger sind ebenfalls eine gute Wahl.

Wenn Sie dieses Muster verwenden, müssen Sie mit Schleifenkonstrukten vorsichtig sein. Unterbrechen Sie innerhalb einer Schleife die Schleife, wenn ein Aufruf fehlschlägt.

Vorteile

  • Dieses Muster erzeugt weniger Schachtelung als das Muster "geschachtelte Ifs".
  • Der gesamte Ablauf der Steuerung ist einfacher zu erkennen.
  • Ressourcen werden an einer Stelle im Code freigegeben.

Nachteile

  • Alle Variablen müssen oben in der Funktion deklariert und initialisiert werden.
  • Wenn ein Aufruf fehlschlägt, führt die Funktion mehrere nicht benötigte Fehlerüberprüfungen durch, anstatt die Funktion sofort zu beenden.
  • Da der Ablauf der Steuerung nach einem Fehler durch die Funktion fortgesetzt wird, müssen Sie im gesamten Funktionstext darauf achten, nicht auf ungültige Ressourcen zuzugreifen.
  • Fehler innerhalb einer Schleife erfordern einen Sonderfall.

Fehler beim Sprung

Testen Sie nach jedem Methodenaufruf auf Fehler (nicht auf Erfolg). Bei Einem Fehler springen Sie zu einer Bezeichnung am unteren Rand der Funktion. Geben Sie nach der Bezeichnung, aber vor dem Beenden der Funktion Ressourcen frei.

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen = NULL;
    IShellItem *pItem = NULL;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFileOpen->Show(NULL);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFileOpen->GetResult(&pItem);
    if (FAILED(hr))
    {
        goto done;
    }

    // Use pItem (not shown).

done:
    // Clean up.
    SafeRelease(&pItem);
    SafeRelease(&pFileOpen);
    return hr;
}

Vorteile

  • Der gesamte Ablauf der Steuerung ist leicht zu erkennen.
  • Wenn Sie nicht zur Bezeichnung gesprungen sind, wird an jedem Punkt im Code nach einer FEHLGESCHLAGENen Überprüfung garantiert, dass alle vorherigen Aufrufe erfolgreich waren.
  • Ressourcen werden an einer Zentralen Stelle im Code freigegeben.

Nachteile

  • Alle Variablen müssen oben in der Funktion deklariert und initialisiert werden.
  • Einige Programmierer verwenden goto nicht gerne in ihrem Code. (Beachten Sie jedoch, dass diese Verwendung von goto stark strukturiert ist. Der Code springt nie außerhalb des aktuellen Funktionsaufrufs.)
  • goto-Anweisungen überspringen Initialisierer.

Auslösen bei Fehler

Anstatt zu einer Bezeichnung zu springen, können Sie eine Ausnahme auslösen, wenn eine Methode fehlschlägt. Dies kann zu einem idiomatischen Stil von C++ führen, wenn Sie es gewohnt sind, ausnahmesicheren Code zu schreiben.

#include <comdef.h>  // Declares _com_error

inline void throw_if_fail(HRESULT hr)
{
    if (FAILED(hr))
    {
        throw _com_error(hr);
    }
}

void ShowDialog()
{
    try
    {
        CComPtr<IFileOpenDialog> pFileOpen;
        throw_if_fail(CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
            CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen)));

        throw_if_fail(pFileOpen->Show(NULL));

        CComPtr<IShellItem> pItem;
        throw_if_fail(pFileOpen->GetResult(&pItem));

        // Use pItem (not shown).
    }
    catch (_com_error err)
    {
        // Handle error.
    }
}

Beachten Sie, dass in diesem Beispiel die CComPtr-Klasse verwendet wird, um Schnittstellenzeiger zu verwalten. Wenn Ihr Code Ausnahmen auslöst, sollten Sie im Allgemeinen dem RAII-Muster (Resource Acquisition is Initialization) folgen. Das heißt, jede Ressource sollte von einem Objekt verwaltet werden, dessen Destruktor garantiert, dass die Ressource ordnungsgemäß freigegeben wird. Wenn eine Ausnahme ausgelöst wird, wird der Destruktor garantiert aufgerufen. Andernfalls kann Ihr Programm ressourcenlecken.

Vorteile

  • Kompatibel mit vorhandenem Code, der die Ausnahmebehandlung verwendet.
  • Kompatibel mit C++-Bibliotheken, die Ausnahmen auslösen, z. B. der Standardvorlagenbibliothek (STL).

Nachteile

  • Erfordert C++-Objekte, um Ressourcen wie Arbeitsspeicher oder Dateihandles zu verwalten.
  • Erfordert ein gutes Verständnis des Schreibens von ausnahmesicherem Code.

Nächste

Modul 3. Windows-Grafiken