Vorgehensweise: Schnittstelle zwischen außergewöhnlichem und nicht außergewöhnlichem Code

In diesem Artikel wird beschrieben, wie Sie eine konsistente Ausnahmebehandlung in C++-Code implementieren und wie Ausnahmen in Und aus Fehlercodes an Ausnahmegrenzen übersetzt werden.

Manchmal muss C++-Code mit Code schnittstelle, der keine Ausnahmen verwendet (nicht außergewöhnlicher Code). Eine solche Schnittstelle wird als Ausnahmegrenze bezeichnet. Sie können z. B. die Win32-Funktion CreateFile im C++-Programm aufrufen. CreateFile löst keine Ausnahmen aus. Stattdessen werden Fehlercodes festgelegt, die von der GetLastError Funktion abgerufen werden können. Wenn Ihr C++-Programm nicht trivial ist, bevorzugen Sie wahrscheinlich eine konsistente ausnahmebasierte Fehlerbehandlungsrichtlinie. Und Sie möchten wahrscheinlich keine Ausnahmen aufgeben, nur weil Sie mit nicht außergewöhnlichem Code zusammenarbeiten. Sie möchten auch keine ausnahmebasierten und nicht ausnahmebasierten Fehlerrichtlinien in Ihrem C++-Code kombinieren.

Aufrufen nicht außergewöhnlicher Funktionen aus C++

Wenn Sie eine Nicht-Ausnahmefunktion von C++ aufrufen, kann diese Funktion in einer C++-Funktion umschlossen werden, die alle Fehler erkennt und dann möglicherweise eine Ausnahme auslöst. Wenn Sie eine solche Wrapperfunktion entwerfen, entscheiden Sie zuerst, welche Art von Ausnahme garantiert werden soll: noexcept, strong oder basic. Als nächstes entwerfen Sie die Funktion so, dass alle Ressourcen, z. B. Dateihandles, ordnungsgemäß freigegeben werden, wenn eine Ausnahme ausgelöst wird. In der Regel bedeutet dies, dass Sie intelligente Zeiger oder ähnliche Ressourcenmanager verwenden, um die Ressourcen zu besitzen. Weitere Informationen zu Entwurfsaspekten finden Sie unter How to: Design for exception safety.

Beispiel

Das folgende Beispiel zeigt C++-Funktionen, die die Win32-Funktionen CreateFile und ReadFile intern verwenden, um zwei Dateien zu öffnen und zu lesen. Die File-Klasse ist ein RAII-Wrapper (Resource Acquisition Is Initialization) für die Dateihandles. Der Konstruktor erkennt eine Bedingung "Datei nicht gefunden" und löst eine Ausnahme aus, um den Fehler auf den Aufrufstapel der ausführbaren Datei C++ zu verteilen (in diesem Beispiel die main() Funktion). Wenn eine Ausnahme ausgelöst wird, nachdem ein File-Objekt vollständig erstellt wurde, ruft der Destruktor automatisch CloseHandle auf, um das Dateihandle freizugeben. (Wenn Es Ihnen lieber ist, können Sie die ATL-Klasse (Active Template Library) CHandle für diesen Zweck oder eine unique_ptr zusammen mit einer benutzerdefinierten Löschfunktion verwenden.) Die Funktionen, die Win32- und CRT-APIs aufrufen, erkennen Fehler und lösen dann C++-Ausnahmen mithilfe der lokal definierten ThrowLastErrorIf Funktion aus, die wiederum die Win32Exception Von der runtime_error Klasse abgeleitete Klasse verwendet. Alle Funktionen in diesem Beispiel bieten eine starke Ausnahmegarantie: Wenn an einem beliebigen Punkt in diesen Funktionen eine Ausnahme ausgelöst wird, werden keine Ressourcen verloren gehen und kein Programmstatus geändert.

// compile with: /EHsc
#include <Windows.h>
#include <stdlib.h>
#include <vector>
#include <iostream>
#include <string>
#include <limits>
#include <stdexcept>

using namespace std;

string FormatErrorMessage(DWORD error, const string& msg)
{
    static const int BUFFERLENGTH = 1024;
    vector<char> buf(BUFFERLENGTH);
    FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, 0, error, 0, buf.data(),
        BUFFERLENGTH - 1, 0);
    return string(buf.data()) + "   ("  + msg  + ")";
}

class Win32Exception : public runtime_error
{
private:
    DWORD m_error;
public:
    Win32Exception(DWORD error, const string& msg)
        : runtime_error(FormatErrorMessage(error, msg)), m_error(error) { }

    DWORD GetErrorCode() const { return m_error; }
};

void ThrowLastErrorIf(bool expression, const string& msg)
{
    if (expression) {
        throw Win32Exception(GetLastError(), msg);
    }
}

class File
{
private:
    HANDLE m_handle;

    // Declared but not defined, to avoid double closing.
    File& operator=(const File&);
    File(const File&);
public:
    explicit File(const string& filename)
    {
        m_handle = CreateFileA(filename.c_str(), GENERIC_READ, FILE_SHARE_READ,
            nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, nullptr);
        ThrowLastErrorIf(m_handle == INVALID_HANDLE_VALUE,
            "CreateFile call failed on file named " + filename);
    }

    ~File() { CloseHandle(m_handle); }

    HANDLE GetHandle() { return m_handle; }
};

size_t GetFileSizeSafe(const string& filename)
{
    File fobj(filename);
    LARGE_INTEGER filesize;

    BOOL result = GetFileSizeEx(fobj.GetHandle(), &filesize);
    ThrowLastErrorIf(result == FALSE, "GetFileSizeEx failed: " + filename);

    if (filesize.QuadPart < (numeric_limits<size_t>::max)()) {
        return filesize.QuadPart;
    } else {
        throw;
    }
}

vector<char> ReadFileVector(const string& filename)
{
    File fobj(filename);
    size_t filesize = GetFileSizeSafe(filename);
    DWORD bytesRead = 0;

    vector<char> readbuffer(filesize);

    BOOL result = ReadFile(fobj.GetHandle(), readbuffer.data(), readbuffer.size(),
        &bytesRead, nullptr);
    ThrowLastErrorIf(result == FALSE, "ReadFile failed: " + filename);

    cout << filename << " file size: " << filesize << ", bytesRead: "
        << bytesRead << endl;

    return readbuffer;
}

bool IsFileDiff(const string& filename1, const string& filename2)
{
    return ReadFileVector(filename1) != ReadFileVector(filename2);
}

#include <iomanip>

int main ( int argc, char* argv[] )
{
    string filename1("file1.txt");
    string filename2("file2.txt");

    try
    {
        if(argc > 2) {
            filename1 = argv[1];
            filename2 = argv[2];
        }

        cout << "Using file names " << filename1 << " and " << filename2 << endl;

        if (IsFileDiff(filename1, filename2)) {
            cout << "+++ Files are different." << endl;
        } else {
            cout<< "=== Files match." << endl;
        }
    }
    catch(const Win32Exception& e)
    {
        ios state(nullptr);
        state.copyfmt(cout);
        cout << e.what() << endl;
        cout << "Error code: 0x" << hex << uppercase << setw(8) << setfill('0')
            << e.GetErrorCode() << endl;
        cout.copyfmt(state); // restore previous formatting
    }
}

Aufrufen von außergewöhnlichem Code aus nicht außergewöhnlichem Code

C++-Funktionen, die als extern "C" von C-Programmen aufgerufen werden können. C++-COM-Server können von Code verwendet werden, der in einer beliebigen Anzahl verschiedener Sprachen geschrieben wurde. Wenn Sie öffentliche ausnahmenbasierte Funktionen in C++ implementieren, die von Nicht-Ausnahmecode aufgerufen werden sollen, darf die C++-Funktion nicht zulassen, dass Ausnahmen wieder an den Aufrufer übergeben werden. Solche Aufrufer haben keine Möglichkeit, C++-Ausnahmen abzufangen oder zu behandeln. Das Programm kann Ressourcen beenden, durchlecken oder zu einem nicht definierten Verhalten führen.

Es wird empfohlen, dass Ihre extern "C" C++-Funktion jede Ausnahme abfangen kann, die sie kennt, und ggf. die Ausnahme in einen Fehlercode konvertieren, den der Aufrufer versteht. Wenn nicht alle möglichen Ausnahmen bekannt sind, sollte die C++-Funktionen über einen catch(...)-Block als letzten Handler verfügen. In einem solchen Fall empfiehlt es sich, einen schwerwiegenden Fehler an den Aufrufer zu melden, da sich Ihr Programm möglicherweise in einem unbekannten und nicht behebbaren Zustand befindet.

The following example shows a function that assumes that any exception that might be thrown is either a Win32Exception or an exception type derived from std::exception. Die Funktion fängt jede Ausnahme dieser Typen ab und gibt die Fehlerinformationen als Win32-Fehlercode an den Aufrufer weiter.

BOOL DiffFiles2(const string& file1, const string& file2)
{
    try
    {
        File f1(file1);
        File f2(file2);
        if (IsTextFileDiff(f1, f2))
        {
            SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
            return FALSE;
        }
        return TRUE;
    }
    catch(Win32Exception& e)
    {
        SetLastError(e.GetErrorCode());
    }

    catch(std::exception& e)
    {
        SetLastError(MY_APPLICATION_GENERAL_ERROR);
    }
    return FALSE;
}

Wenn Sie von Ausnahmen in Fehlercodes konvertieren, gibt es ein potenzielles Problem: Fehlercodes enthalten häufig nicht die Fülle von Informationen, die eine Ausnahme speichern kann. Um dieses Problem zu beheben, können Sie einen catch Block für jeden bestimmten Ausnahmetyp bereitstellen, der ausgelöst werden kann, und die Protokollierung durchführen, um die Details der Ausnahme aufzuzeichnen, bevor sie in einen Fehlercode konvertiert wird. Dieser Ansatz kann sich wiederholenden Code erstellen, wenn alle mehrere Funktionen denselben Satz von catch Blöcken verwenden. Eine gute Möglichkeit, Codewiederholungen zu vermeiden, besteht darin, diese Blöcke in eine private Hilfsfunktion umzugestalten, die die try und catch Blöcke implementiert und ein Funktionsobjekt akzeptiert, das im try Block aufgerufen wird. Übergeben Sie den Code in jeder öffentlichen Funktion als Lambda-Ausdruck an die Hilfsfunktion.

template<typename Func>
bool Win32ExceptionBoundary(Func&& f)
{
    try
    {
        return f();
    }
    catch(Win32Exception& e)
    {
        SetLastError(e.GetErrorCode());
    }
    catch(const std::exception& e)
    {
        SetLastError(MY_APPLICATION_GENERAL_ERROR);
    }
    return false;
}

Das folgende Beispiel zeigt, wie der Lambdaausdruck geschrieben wird, der das Funktionselement definiert. Ein Lambda-Ausdruck ist häufig einfacher zu lesen als Code, der ein benanntes Funktionsobjekt aufruft.

bool DiffFiles3(const string& file1, const string& file2)
{
    return Win32ExceptionBoundary([&]() -> bool
    {
        File f1(file1);
        File f2(file2);
        if (IsTextFileDiff(f1, f2))
        {
            SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
            return false;
        }
        return true;
    });
}

Weitere Informationen zu Lambdaausdrücken finden Sie unter Lambdaausdrücke.

Aufrufen von außergewöhnlichem Code über nicht außergewöhnlichen Code

Es ist möglich, aber nicht empfohlen, Ausnahmen über Ausnahmecode hinweg auszuwerfen. Ihr C++-Programm kann beispielsweise eine Bibliothek aufrufen, die Rückruffunktionen verwendet, die Sie bereitstellen. Unter bestimmten Umständen können Sie Ausnahmen von den Rückruffunktionen über den nicht außergewöhnlichen Code auslösen, den Ihr ursprünglicher Aufrufer verarbeiten kann. Die Umstände, unter denen Ausnahmen erfolgreich funktionieren können, sind jedoch streng. Sie müssen den Bibliothekscode auf eine Weise kompilieren, mit der stapelseitige Semantik erhalten bleibt. Der Ausnahmecode kann nichts tun, das die C++-Ausnahme möglicherweise abfangen kann. Außerdem kann der Bibliothekscode zwischen dem Aufrufer und dem Rückruf keine lokalen Ressourcen zuordnen. Der Code, der nicht ausnahmefähig ist, kann z. B. keine Lokalen aufweisen, die auf den zugeordneten Heap-Speicher verweisen. Diese Ressourcen werden geleert, wenn der Stapel aufgewockt wird.

Diese Anforderungen müssen erfüllt sein, um Ausnahmen für nicht ausnahmefähigen Code auszuwerfen:

  • Sie können den gesamten Codepfad über den nicht ausnahmefähigen Code mithilfe von /EHs,
  • Es gibt keine lokal zugeordneten Ressourcen, die beim Aufteilen des Stapels verloren gehen können.
  • Der Code verfügt nicht über __except strukturierte Ausnahmehandler, die alle Ausnahmen abfangen.

Da das Auslösen von Ausnahmen über nicht außergewöhnlichen Code fehleranfällig ist und zu schwierigen Debuggingproblemen führen kann, wird dies nicht empfohlen.

Siehe auch

Bewährte Methoden für moderne C++-Methoden für Ausnahmen und Fehlerbehandlung
How to: Design for exception safety