Практическое руководство. Интерфейс между исключительным и неисключаемого кода

В этой статье описывается, как реализовать согласованную обработку исключений в коде C++ и как преобразовать исключения в коды ошибок и из кодов ошибок на границах исключений.

Иногда код C++ должен интерфейсировать с кодом, который не использует исключения (не исключительный код). Такой интерфейс называется границей исключения. Например, может потребоваться вызвать функцию CreateFile Win32 в программе C++. CreateFile не вызывает исключений. Вместо этого он задает коды ошибок, которые можно получить функцией GetLastError . Если программа C++ не является тривиальной, скорее всего, вы предпочитаете иметь согласованную политику обработки ошибок на основе исключений. И, вероятно, вы не хотите отказаться от исключений только потому, что интерфейс с неисключаемого кода. Вы также не хотите смешивать политики ошибок на основе исключений и неисключения в коде C++.

Вызов неисключаемых функций из C++

При вызове функции, отличной от C++, идея заключается в том, чтобы упаковать эту функцию в функцию C++, которая обнаруживает любые ошибки, а затем, возможно, создает исключение. При разработке такой функции-оболочки сначала определите тип гарантии исключения: noexcept, strong или basic. Во-вторых, создайте функцию таким образом, чтобы все ресурсы, например дескрипторы файлов, были правильно освобождены при возникновении исключения. Как правило, это означает, что для владения ресурсами используются интеллектуальные указатели или аналогичные диспетчеры ресурсов. Дополнительные сведения о рекомендациях по проектированию см. в статье "Практическое руководство. Проектирование для обеспечения безопасности исключений".

Пример

В следующем примере показаны функции C++, которые используют Win32 CreateFile и ReadFile функции внутренне для открытия и чтения двух файлов. Класс File является приобретением ресурсов — это оболочка инициализации (RAII) для дескрипторов файлов. Его конструктор обнаруживает условие "файл не найден" и создает исключение для распространения ошибки в стеке вызовов исполняемого файла C++ (в этом примере main() функция). Если исключение создается после полного File создания объекта, деструктор автоматически вызывает CloseHandle деструктор для освобождения дескриптора файла. (Если вы предпочитаете, можно использовать класс библиотеки шаблонов Active Template Library (ATL) CHandle для этой же цели или unique_ptr вместе с пользовательской функцией удаления.) Функции, вызывающие API Win32 и CRT, обнаруживают ошибки, а затем вызывают исключения C++ с помощью локально определенной ThrowLastErrorIf функции, которая, в свою очередь, использует Win32Exception класс, производный от runtime_error класса. Все функции в этом примере обеспечивают надежную гарантию исключения: если исключение создается в любой момент в этих функциях, ресурсы не утечки и состояние программы не изменяется.

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

Вызов исключительного кода из неисключаемого кода

Функции C++, объявленные как extern "C" могут вызываться программами C. Com-серверы C++ могут использоваться кодом, написанным на любом количестве различных языков. При реализации общедоступных функций с поддержкой исключений в C++ для вызова неисключаемым кодом функция C++ не должна разрешать распространение исключений обратно вызывающему объекту. Такие вызывающие методы не могут перехватывать или обрабатывать исключения C++. Программа может завершить работу, утечку ресурсов или вызвать неопределенное поведение.

Рекомендуется, чтобы функция extern "C" C++ специально перехватывала каждое исключение, которое он знает, как обрабатывать и при необходимости преобразовывать исключение в код ошибки, который распознает вызывающий объект. Если не все потенциальные исключения известны, функция C++ должна иметь catch(...) блок в качестве последнего обработчика. В таком случае лучше сообщить о неустранимой ошибке вызывающему объекту, так как ваша программа может находиться в неизвестном и невосстановленном состоянии.

В следующем примере показана функция, предполагающая, что любое исключение, которое может быть создано, является либо Win32Exception типом исключения, производным от std::exception. Функция перехватывает любое исключение этих типов и распространяет сведения об ошибке в виде кода ошибки Win32 вызывающей стороны.

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

При преобразовании исключений в коды ошибок возникает потенциальная проблема: коды ошибок часто не содержат богатых сведений, которые может хранить исключение. Чтобы устранить эту проблему, можно указать catch блок для каждого конкретного типа исключения, который может быть создан, и выполнить ведение журнала для записи сведений об исключении перед преобразованием в код ошибки. Этот подход может создать повторяющийся код, если несколько функций используют один и тот же набор catch блоков. Хорошим способом избежать повторения кода является рефакторинг этих блоков в одну функцию частной служебной программы, которая реализует try и блокирует и catch принимает объект функции, вызываемый в блоке try . В каждой общедоступной функции передайте код в функцию служебной программы в виде лямбда-выражения.

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

В следующем примере показано, как написать лямбда-выражение, определяющее functor. Лямбда-выражение часто проще считать встроенным, чем код, который вызывает именованный объект функции.

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

Дополнительные сведения о лямбда-выражениях см. в разделе Лямбда-выражения.

Вызов исключительного кода с помощью неисключаемого кода из исключительного кода

Возможно, но не рекомендуется создавать исключения в неузнаемом коде. Например, программа C++ может вызывать библиотеку, использующую предоставляемые функции обратного вызова. В некоторых случаях можно вызывать исключения из функций обратного вызова в неисключаемом коде, который может обрабатывать исходный вызывающий объект. Однако обстоятельства, когда исключения могут работать успешно, являются строгими. Необходимо скомпилировать код библиотеки таким образом, чтобы сохранить стек очистки семантики. Неузнающий от исключения код не может ничего сделать, что может захватить исключение C++. Кроме того, код библиотеки между вызывающим и обратным вызовом не может выделить локальные ресурсы. Например, код, который не учитывает исключения, не может содержать локальные параметры, указывающие на выделенную память кучи. Эти ресурсы утечки возникают, когда стек раскрыт.

Эти требования должны быть выполнены для создания исключений в коде, отличном от исключений:

  • Вы можете создать весь путь к коду в коде, не поддерживающем исключения, с помощью /EHs.
  • Не существует локальных выделенных ресурсов, которые могут утечь при стеке,
  • В коде нет __except структурированных обработчиков исключений, перехватывающих все исключения.

Так как создание исключений в неисключаемом коде подвержено ошибкам и может вызвать сложные проблемы отладки, мы не рекомендуем его.

См. также

Современные рекомендации по C++ по исключению и обработке ошибок
Практическое руководство. Проектирование безопасности исключений