如何:异常和非异常代码之间的接口

本文介绍如何在 C++ 代码中实现一致的异常处理,以及如何在异常边界将异常和错误代码进行互相转换。

有时 C++ 代码必须与不使用异常的代码(非异常代码)进行交互。 此类接口称为异常边界。 例如,您可能希望在 C++程序中调用 Win32 的函数 CreateFileCreateFile 不会引发异常。 相反,它会设置 GetLastError 函数可能检索到的错误代码。 如果你的 C++ 程序很重要,你可能更愿意部署一致的基于异常的错误处理策略。 此外,你可能不想仅仅因为你与非异常代码进行交互而放弃异常。 你也不希望在 C ++ 代码中将基于异常的错误策略与基于非异常的错误策略混合。

从 C++ 调用非异常函数

当您从 C++ 调用非异常函数时,您的想法是将该函数包装在检测任何错误的 C++函数中,然后可能引发异常。 当设计此类包装器函数时,首先应确定提供的异常保证的类型:noexcept、strong 或 basic。 其次,设计函数以使得异常引发时能够正确发布所有资源(例如文件句柄)。 通常,它意味着你可使用智能指针或类似的资源管理器来拥有资源。 有关设计注意事项的详细信息,请参阅如何:设计以实现异常安全性

示例

以下示例演示使用 Win32 CreateFileReadFile函数在内部打开并读取两个文件的 C++ 函数。 File 类是文件句柄的“资源获取即是初始化”(RAII) 包装器。 其构造函数检测到“找不到文件”条件,并引发了在 C++ 可执行文件的调用堆栈(本示例中为 main() 函数)中向上传播错误的异常。 如果在完全构造好 File 对象后引发了异常,析构函数将自动调用 CloseHandle 以释放文件句柄。 (如果你愿意,可以将活动模板库 (ATL) CHandle 类用于此同一目的,或者将 unique_ptr 与自定义删除函数一起使用。) 调用 Win32 和 CRT API 的函数检测到错误,然后使用本地定义的 ThrowLastErrorIf 函数引发 C++ 异常,该异常反过来使用派生自 runtime_error 类的 Win32Exception 类。 本示例中的所有函数提供了强力的异常保证:当这些函数中的任何点上引发异常时,资源不会泄露,程序状态也不会修改。

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

从非异常代码调用异常代码

声明为 extern "C" 的 C++ 函数可由 C 程序调用。 C++ COM 服务器可由采用任意数量的不同语言编写的代码使用。 在使用 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 块,此方法可能产生重复代码。 避免代码重复的一个好方法是,将这些块重构到一个实现 trycatch 块并接受 try 块中调用的函数对象的专用实用工具函数中。 在每个公用函数中,将代码传递到实用工具函数以作为 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;
}

以下示例演示如何编写函数定义的 lambda 表达式。 lambda 表达式通常比调用命名函数对象的代码更容易内联读取。

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

若要详细了解 lambda 表达式,请参阅 lambda 表达式

从异常代码通过非异常代码调用异常代码

在无法识别异常的代码中引发异常是可能的,但不建议这样做。 例如,C++ 程序可能会调用一个使用你提供的回调函数的库。 在某些情况下,可以在原始调用方可处理的非异常代码中从回调函数引发异常。 但是,异常能够成功运行的环境是严格的。 必须以保留堆栈展开语义的方式编译库代码。 无法识别异常的代码不能执行任何可能捕获 C++ 异常的操作。 此外,调用方和回调之间的库代码无法分配本地资源。 例如,无法识别异常的代码不能有指向已分配堆内存的局部变量。 当堆栈展开时,这些资源会遭到泄露。

必须满足以下要求才能在可识别非异常的代码中引发异常:

  • 你可以使用 /EHs 在可识别非异常的代码中构建整个代码路径,
  • 在堆栈展开时,不会有任何本地分配的资源出现泄露,
  • 代码没有任何用于捕获所有异常的 __except 结构化异常处理程序。

由于在非异常代码中引发异常很容易出错,并且可能会增加调试难度,因此不建议采用这种方式。

另请参阅

现代 C++ 处理异常和错误的最佳做法
如何:设计以实现异常安全性