Fehlerbehandlung in COM (Erste Schritte mit Win32 und C++)
COM verwendet HRESULT-Werte, um den Erfolg oder Fehlschlag eines Methoden- oder Funktionsaufrufs anzugeben. Verschiedene SDK-Header definieren verschiedene HRESULT-Konstanten. In WinError.h wird ein allgemeiner Satz von systemweiten Codes definiert. In der folgenden Tabelle werden einige dieser systemweiten Rückgabecodes aufgeführt.
Konstante | 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 nicht korrekterweise für einen Zeigerwert ü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 beides Erfolgscodes. Wahrscheinlich geben 99 % der COM-Methoden S_OK zurück, wenn sie erfolgreich sind; lassen Sie sich hiervon jedoch nicht in die Irre führen. Eine Methode kann andere Erfolgscodes zurückgeben. Testen Sie dies daher stets mithilfe des Makros SUCCEEDED oder FAILED. Der folgende Beispielcode zeigt den falschen und den richtigen Weg für das Testen auf den Erfolg eines Funktionsaufrufs.
// 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 sollte genannt werden. Einige Methoden verwenden S_FALSE, um eine negative Bedingung anzugeben, die kein Fehler ist, grob gesagt. Dies kann auch „no-op“ bedeuten – die Methode war erfolgreich, hatte jedoch keine Wirkung. Beispielsweise gibt die Funktion CoInitializeEx den Wert S_FALSE zurück, wenn Sie diese ein zweites Mal aus demselben Thread aufrufen. Wenn Sie im Code zwischen S_OK und S_FALSE unterscheiden müssen, sollten Sie den Wert direkt testen. Sie sollten jedoch weiter FAILED oder SUCCEEDED zur Behandlung der verbleibenden Fälle verwenden, 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 sind für eine bestimmte Funktion oder ein bestimmtes Subsystem von Windows spezifisch. Beispielsweise definiert die Direct2D-Grafik-API den Fehlercode D2DERR_UNSUPPORTED_PIXEL_FORMAT, der angibt, dass das Programm ein nicht unterstütztes Pixelformat verwendet hat. Die Windows-Dokumentation enthält häufig eine Liste bestimmter Fehlercodes, die von einer Methode zurückgegeben werden können. Sie sollten diese Listen jedoch nicht als endgültig betrachten. Eine Methode kann stets einen HRESULT-Wert zurückgeben, der nicht in der Dokumentation aufgeführt ist. Verwenden Sie erneut die Makros SUCCEEDED und FAILED. Schließen Sie auch einen Standardfall ein, wenn Sie auf einen bestimmten Fehlercode testen.
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 beschrieben. Jedes Muster hat Vor- und Nachteile. Die Wahl des Musters ist in gewisser Weise eine Frage der Präferenz. Ein vorhandenes Projekt, an dem sie arbeiten, besitzt möglicherweise bereits Codierungsrichtlinien, die ein bestimmtes Format festlegen. Unabhängig davon, welches Muster Sie übernehmen, beachtet ein robuster Code die folgenden Regeln.
- Lassen Sie für jede Methode oder Funktion, die einen HRESULT-Wert zurückgibt, den Rückgabewert überprüfen, bevor die Verarbeitung fortgesetzt wird.
- Geben Sie Ressourcen nach der Verwendung frei.
- Versuchen Sie nicht, auf ungültige oder nicht initialisierte Ressourcen zuzugreifen, z. B. NULL-Zeiger.
- Versuchen Sie nicht, eine Ressource zu verwenden, nachdem sie freigegeben wurde.
Vor dem Hintergrund dieser vier Regeln werden hier vier Muster für die Behandlung von Fehlern aufgelistet.
Verschachtelte IF-Anweisungen
Verwendet nach jedem Aufruf, der einen HRESULT-Wert zurückgibt, eine IF-Anweisung, um auf Erfolg zu testen. Platziert dann den nächsten Methodenaufruf im Bereich der IF-Anweisung. Weitere IF-Anweisungen können so tief wie erforderlich geschachtelt werden. Die früheren Codebeispiele in diesem Modul haben alle dieses Muster verwendet. Hier wird es erneut gezeigt:
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 in minimalem Umfang deklariert werden. Beispielsweise wird pItem erst deklariert, wenn sie verwendet wird.
- In jeder IF-Anweisung sind bestimmte Invarianten wahr: Alle vorherigen Aufrufe waren erfolgreich, und alle abgerufenen Ressourcen sind weiterhin gültig. Wenn im vorherigen Beispiel das Programm die innerste IF-Anweisung erreicht, sind sowohl pItem als auch pFileOpen als gültig bekannt.
- Der Zeitpunkt, an dem Schnittstellenzeiger und andere Ressourcen freigegeben werden, ist eindeutig. Gibt eine Ressource am Ende der IF-Anweisung frei, die unmittelbar auf den Aufruf folgt, der die Ressource abgerufen hat.
Nachteile
- Manche Programmierer*innen empfinden es als schwierig, tiefe Verschachtelungen zu lesen.
- Die Fehlerbehandlung ist mit anderen Branching- und Schleifenanweisungen vermischt. Dies kann dazu führen, dass die Programmlogik insgesamt schwieriger zu verfolgen ist.
Kaskadierende IF-Anweisungen
Verwendet nach jedem Methodenaufruf eine IF-Anweisung, um auf Erfolg zu testen. Wenn die Methode erfolgreich ist, wird der nächste Methodenaufruf innerhalb des IF-Blocks platziert. Statt jedoch weitere IF-Anweisungen zu verschachteln, wird jeder folgende SUCCEEDED-Test nach dem jeweils vorangehenden IF-Block platziert. Wenn eine Methode fehlschlägt, schlagen alle verbleibenden SUCCEEDED-Tests ebenfalls 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;
}
In diesem Muster werden Ressourcen erst ganz am Ende der Funktion freigegeben. Wenn ein Fehler auftritt, sind einige Zeiger möglicherweise ungültig, wenn die Ausführung der Funktion beendet wird. Der Aufruf von Release für einen ungültigen Zeiger führt zum Absturz des Programms (oder schlimmer). Daher müssen alle Zeiger zu NULL initialisiert werden und es muss geprüft werden, ob sie NULL sind, bevor sie freigegeben werden. In diesem Beispiel wird die Funktion SafeRelease
verwendet. Intelligente Zeiger sind ebenfalls eine gute Wahl.
Wenn Sie dieses Muster verwenden, müssen Sie bei Schleifenkonstrukten vorsichtig sein. Wenn ein Aufruf innerhalb einer Schleife fehlschlägt, wird die Schleife unterbrochen.
Vorteile
- Dieses Muster führt zu einer geringeren Verschachtelung als das Muster „Verschachtelte IF-Anweisungen“.
- Der allgemeine Steuerungsfluss kann leichter erkannt werden.
- Ressourcen werden nur an einem einzigen Punkt im Code freigegeben.
Nachteile
- Alle Variablen müssen zu Beginn der Funktion deklariert und initialisiert werden.
- Wenn ein Aufruf fehlschlägt, führt die Funktion mehrere nicht benötigte Fehlerprüfungen durch; die Ausführung der Funktion wird nicht sofort beendet.
- Da nach einem Fehler der Steuerungsfluss durch die Funktion hindurch fortgesetzt wird, darf in der gesamten Funktion kein Zugriff auf ungültige Ressourcen erfolgen.
- Fehler innerhalb einer Schleife erfordern eine besondere Behandlung.
Sprung bei Fehler
Testet nach jedem Methodenaufruf auf Fehler (d. h. auf einen nicht erfolgreichen Aufruf). Springt bei einem Fehler zu einem Label am Ende der Funktion. Gibt nach dem Label, jedoch 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 allgemeine Steuerungsfluss kann leichter erkannt werden.
- Wenn an keinem Punkt im Code nach einer FAILED-Überprüfung ein Sprung zum Label erfolgt, waren alle vorherigen Aufrufe mit Sicherheit erfolgreich.
- Ressourcen werden nur an einer einzigen Stelle im Code freigegeben.
Nachteile
- Alle Variablen müssen zu Beginn der Funktion deklariert und initialisiert werden.
- Einige Programmierer*innen verwenden jedoch goto nicht gerne im Code. (Beachten Sie jedoch, dass diese Verwendung von goto hoch strukturiert ist. Der Code springt niemals außerhalb des aktuellen Funktionsaufrufs.)
- goto-Anweisungen überspringen Initialisierer.
Auslösung bei Fehler
Statt zu einem Label zu springen, wird beim Fehlschlag einer Methode eine Ausnahme ausgelöst. Hierdurch kann ein idiomatischeres Format für C++ erzeugt werden, 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 Klasse CComPtr für die Verwaltung von Schnittstellenzeigern verwendet wird. Wenn Ihr Code Ausnahmen auslöst, sollten Sie grundsätzlich das Muster „Ressourcenbelegung ist Initialisierung“ (Resource Acquisition is Initialization, RAII) verwenden. Dies bedeutet, dass jede Ressource von einem Objekt verwaltet werden sollte, dessen Destruktor die korrekte Freigabe der Ressource garantiert. Bei Auslösung einer Ausnahme wird der Destruktor garantiert aufgerufen. Andernfalls könnte es zu einem Ressourcenverlust für Ihr Programm kommen.
Vorteile
- Kompatibel mit vorhandenem Code, der die Ausnahmebehandlung verwendet.
- Kompatibel mit C++-Bibliotheken, die Ausnahmen auslösen, z. B. die Standardvorlagenbibliothek (Standard Template Library, STL).
Nachteile
- Erfordert C++-Objekte für die Verwaltung von Ressourcen. z. B. Speicher- oder Dateihandles.
- Erfordert ein gutes Wissen über das Schreiben von ausnahmesicherem Code.
Nächste