如何:异常和非异常代码之间的接口
本文介绍如何在 C++ 代码中实现一致的异常处理,以及如何在异常边界将异常和错误代码进行互相转换。
有时 C++ 代码必须与不使用异常的代码(非异常代码)进行交互。 此类接口称为异常边界。 例如,您可能希望在 C++程序中调用 Win32 的函数 CreateFile
。 CreateFile
不会引发异常。 相反,它会设置 GetLastError
函数可能检索到的错误代码。 如果你的 C++ 程序很重要,你可能更愿意部署一致的基于异常的错误处理策略。 此外,你可能不想仅仅因为你与非异常代码进行交互而放弃异常。 你也不希望在 C ++ 代码中将基于异常的错误策略与基于非异常的错误策略混合。
从 C++ 调用非异常函数
当您从 C++ 调用非异常函数时,您的想法是将该函数包装在检测任何错误的 C++函数中,然后可能引发异常。 当设计此类包装器函数时,首先应确定提供的异常保证的类型:noexcept、strong 或 basic。 其次,设计函数以使得异常引发时能够正确发布所有资源(例如文件句柄)。 通常,它意味着你可使用智能指针或类似的资源管理器来拥有资源。 有关设计注意事项的详细信息,请参阅如何:设计以实现异常安全性。
示例
以下示例演示使用 Win32 CreateFile
和 ReadFile
函数在内部打开并读取两个文件的 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
块,此方法可能产生重复代码。 避免代码重复的一个好方法是,将这些块重构到一个实现 try
和 catch
块并接受 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
结构化异常处理程序。
由于在非异常代码中引发异常很容易出错,并且可能会增加调试难度,因此不建议采用这种方式。
另请参阅
反馈
https://aka.ms/ContentUserFeedback。
即将发布:在整个 2024 年,我们将逐步淘汰作为内容反馈机制的“GitHub 问题”,并将其取代为新的反馈系统。 有关详细信息,请参阅:提交和查看相关反馈