Instrukcje: interfejs między kodem wyjątkowym i innym niż wyjątkowy

W tym artykule opisano sposób implementowania spójnej obsługi wyjątków w kodzie języka C++ oraz sposobu tłumaczenia wyjątków na i z kodów błędów na granicach wyjątków.

Czasami kod C++ musi być interfejsem z kodem, który nie korzysta z wyjątków (kod inny niż wyjątkowy). Taki interfejs jest znany jako granica wyjątku. Na przykład możesz wywołać funkcję CreateFile Win32 w programie C++. CreateFile nie zgłasza wyjątków. Zamiast tego ustawia kody błędów, które można pobrać przez GetLastError funkcję. Jeśli program C++ nie jest trywialny, prawdopodobnie wolisz mieć spójne zasady obsługi błędów oparte na wyjątkach. I prawdopodobnie nie chcesz porzucać wyjątków tylko dlatego, że interfejs z kodem innym niż wyjątkowy. Nie chcesz również mieszać zasad błędów opartych na wyjątkach i nienależących do wyjątków w kodzie języka C++.

Wywoływanie funkcji innych niż wyjątkowe z poziomu języka C++

W przypadku wywoływania funkcji innej niż wyjątkowa z języka C++, chodzi o opakowywanie tej funkcji w funkcji języka C++, która wykrywa błędy, a następnie może zgłosić wyjątek. Podczas projektowania takiej funkcji otoki należy najpierw zdecydować, który typ wyjątku gwarantuje zapewnienie: noexcept, strong lub basic. Po drugie zaprojektuj funkcję tak, aby wszystkie zasoby, na przykład dojścia do plików, były poprawnie zwalniane, jeśli zostanie zgłoszony wyjątek. Zazwyczaj oznacza to, że używasz inteligentnych wskaźników lub podobnych menedżerów zasobów do posiadania zasobów. Aby uzyskać więcej informacji na temat zagadnień dotyczących projektowania, zobacz Jak projektować pod kątem bezpieczeństwa wyjątków.

Przykład

W poniższym przykładzie przedstawiono funkcje języka C++, które używają systemu Win32 CreateFile i ReadFile funkcji wewnętrznie do otwierania i odczytywania dwóch plików. Klasa File jest pozyskiwaniem zasobów jest inicjalizacja (RAII) otoki dla uchwytów plików. Jego konstruktor wykrywa warunek "nie znaleziono pliku" i zgłasza wyjątek propagacji błędu w górę stosu wywołań pliku wykonywalnego języka C++ (w tym przykładzie main() funkcja). Jeśli wyjątek jest zgłaszany po File całkowitym skonstruowaniu obiektu, destruktor automatycznie wywołuje CloseHandle polecenie zwolnienia uchwytu pliku. (Jeśli wolisz, możesz użyć klasy Active Template Library (ATL) CHandle w tym samym celu lub unique_ptr razem z funkcją usuwania niestandardowego. Funkcje wywołujące interfejsy API Win32 i CRT wykrywają błędy, a następnie zgłaszają wyjątki języka C++ przy użyciu funkcji zdefiniowanej ThrowLastErrorIf lokalnie, która z kolei używa Win32Exception klasy pochodzącej runtime_error z klasy. Wszystkie funkcje w tym przykładzie zapewniają silną gwarancję wyjątku: jeśli w dowolnym momencie w tych funkcjach zostanie zgłoszony wyjątek, żadne zasoby nie zostaną ujawnione i żaden stan programu nie zostanie zmodyfikowany.

// 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
    }
}

Wywoływanie wyjątkowego kodu z kodu innego niż wyjątkowy

Funkcje języka C++, które są deklarowane jako extern "C" mogą być wywoływane przez programy języka C. Serwery COM języka C++ mogą być używane przez kod napisany w dowolnej liczbie różnych języków. W przypadku implementowania publicznych funkcji obsługujących wyjątki w języku C++ do wywoływania przez kod inny niż wyjątkowy funkcja języka C++ nie może zezwalać na propagowanie żadnych wyjątków z powrotem do obiektu wywołującego. Takie osoby wywołujące nie mają możliwości przechwytywania ani obsługi wyjątków języka C++. Program może zakończyć działanie, wyciek zasobów lub spowodować niezdefiniowane zachowanie.

Zalecamy, extern "C" aby funkcja języka C++ przechwyciła każdy wyjątek, który wie, jak obsługiwać, i, jeśli jest to konieczne, przekonwertuj wyjątek na kod błędu, który rozumie obiekt wywołujący. Jeśli nie wszystkie potencjalne wyjątki są znane, funkcja języka C++ powinna mieć blok jako ostatnią procedurę catch(...) obsługi. W takim przypadku najlepiej zgłosić błąd krytyczny do wywołującego, ponieważ program może być w stanie nieznanym i nieodwracalnym.

W poniższym przykładzie pokazano funkcję, która zakłada, że każdy wyjątek, który może zostać zgłoszony, to Win32Exception typ wyjątku lub typ wyjątku pochodzący z std::exceptionklasy . Funkcja przechwytuje wszelkie wyjątki tych typów i propaguje informacje o błędzie jako kod błędu Win32 do obiektu wywołującego.

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;
}

Podczas konwertowania z wyjątków na kody błędów istnieje potencjalny problem: Kody błędów często nie zawierają bogactwa informacji, które mogą przechowywać wyjątek. Aby rozwiązać ten problem, możesz podać catch blok dla każdego określonego typu wyjątku, który może zostać zgłoszony, i wykonać rejestrowanie w celu zarejestrowania szczegółów wyjątku, zanim zostanie przekonwertowany na kod błędu. Takie podejście może tworzyć powtarzalny kod, jeśli wiele funkcji używa tego samego zestawu bloków catch . Dobrym sposobem uniknięcia powtórzeń kodu jest refaktoryzacja tych bloków w jedną prywatną funkcję narzędziową, która implementuje try bloki i catch akceptuje obiekt funkcji wywoływany w try bloku. W każdej funkcji publicznej przekaż kod do funkcji narzędzia jako wyrażenie lambda.

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;
}

W poniższym przykładzie pokazano, jak napisać wyrażenie lambda definiujące functor. Wyrażenie lambda jest często łatwiejsze do odczytania w tekście niż kod, który wywołuje nazwany obiekt funkcji.

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;
    });
}

Aby uzyskać więcej informacji na temat wyrażeń lambda, zobacz Wyrażenia lambda.

Wywoływanie wyjątkowego kodu za pomocą kodu wyjątkowego

Istnieje możliwość, ale nie jest to zalecane, aby zgłaszać wyjątki w kodzie nieświadomym wyjątków. Na przykład program C++ może wywołać bibliotekę korzystającą z funkcji wywołania zwrotnego, które udostępniasz. W niektórych okolicznościach można zgłaszać wyjątki z funkcji wywołania zwrotnego w nietypowym kodzie, który może obsłużyć oryginalny obiekt wywołujący. Jednak sytuacje, w których wyjątki mogą działać pomyślnie, są ścisłe. Należy skompilować kod biblioteki w sposób, który zachowuje semantyka odwijania stosu. Kod nieświadomy wyjątku nie może wykonywać żadnych czynności, które mogą wychwycić wyjątek C++. Ponadto kod biblioteki między obiektem wywołującym i wywołaniem zwrotnym nie może przydzielić zasobów lokalnych. Na przykład kod, który nie jest świadomy wyjątków, nie może mieć ustawień lokalnych, które wskazują przydzieloną pamięć stert. Te zasoby są wyciekane, gdy stos jest nieuczyszczone.

Te wymagania muszą zostać spełnione, aby zgłaszać wyjątki w kodzie bez wyjątków:

  • Całą ścieżkę kodu można utworzyć w kodzie bez wyjątków przy użyciu polecenia /EHs,
  • Nie ma żadnych zasobów przydzielonych lokalnie, które mogą wyciekać, gdy stos jest niezwiązany,
  • Kod nie ma żadnych __except procedur obsługi wyjątków strukturalnych, które przechwytują wszystkie wyjątki.

Ponieważ zgłaszanie wyjątków w kodzie innym niż wyjątkowy jest podatne na błędy i może powodować trudne wyzwania debugowania, nie zalecamy go.

Zobacz też

Nowoczesne najlepsze rozwiązania dotyczące języka C++ dotyczące wyjątków i obsługi błędów
Instrukcje: Projektowanie pod kątem bezpieczeństwa wyjątków