1. 异常处理机制的本质与价值
在C++的世界里,异常处理就像代码的消防系统。当程序运行过程中遇到不可预知的错误(比如内存不足、文件不存在、网络中断等),异常机制提供了一种优雅的"逃生通道",让程序能够从错误中恢复或至少有序终止,而不是直接崩溃。与传统的错误码返回方式相比,异常处理将正常逻辑与错误处理分离,使得代码更清晰、更易维护。
我见过太多因为缺乏良好异常处理而导致的生产事故。有一次线上服务因为一个未捕获的异常直接崩溃,导致数百万用户无法访问。事后排查发现,仅仅是因为某个配置文件读取失败时没有正确处理异常。这让我深刻认识到,异常处理绝不是可有可无的语法糖,而是工程实践中必须掌握的生存技能。
2. C++异常处理基础语法
2.1 try-catch块的基本结构
异常处理的核心是try-catch语句块。基本语法如下:
cpp复制try {
// 可能抛出异常的代码
throw std::runtime_error("Something went wrong");
} catch (const std::exception& e) {
// 处理异常
std::cerr << "Error: " << e.what() << std::endl;
}
这里的throw相当于"点火"——主动触发一个异常,而catch则是"灭火器",负责捕获并处理异常。值得注意的是,C++标准库提供了std::exception作为所有标准异常的基类,自定义异常通常也应继承它。
2.2 异常类型与捕获顺序
C++允许抛出任何类型的对象作为异常——基本类型、字符串、自定义类等。但工程实践中,我们通常使用标准异常或自定义异常类:
cpp复制class MyCustomException : public std::runtime_error {
public:
MyCustomException(const std::string& msg)
: std::runtime_error(msg) {}
};
// 使用示例
throw MyCustomException("Custom error occurred");
捕获异常时,顺序很重要——catch子句会按顺序匹配,所以应该从最具体的异常到最通用的异常:
cpp复制try {
// ...
} catch (const MyCustomException& e) {
// 先捕获自定义异常
} catch (const std::runtime_error& e) {
// 然后是运行时错误
} catch (const std::exception& e) {
// 最后是通用异常
} catch (...) {
// 捕获所有其他异常(慎用!)
}
警告:
catch(...)会捕获所有异常,包括系统级异常(如访问违例)。除非你知道自己在做什么,否则应该避免使用,因为它可能掩盖严重问题。
3. 异常安全与RAII原则
3.1 异常安全等级
异常安全是指代码在抛出异常时仍能保持正确状态的能力。通常分为三个等级:
- 基本保证:无论是否抛出异常,程序都处于有效状态(无资源泄漏,对象仍可销毁)
- 强保证:操作要么完全成功,要么回滚到操作前的状态(事务性)
- 不抛出保证:操作保证不会抛出任何异常
3.2 RAII:异常安全的基石
RAII(Resource Acquisition Is Initialization)是C++管理资源的黄金法则。其核心思想是:资源获取即初始化,资源释放由析构函数自动完成。这使得即使发生异常,资源也能被正确释放。
cpp复制class FileHandle {
FILE* file;
public:
explicit FileHandle(const char* filename)
: file(fopen(filename, "r")) {
if (!file) throw std::runtime_error("File open failed");
}
~FileHandle() { if (file) fclose(file); }
// 禁用拷贝(简化示例)
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
void processFile() {
FileHandle fh("data.txt"); // 资源获取
// 使用文件...
// 即使这里抛出异常,fh的析构函数也会自动关闭文件
}
现代C++中的智能指针(std::unique_ptr、std::shared_ptr)就是RAII的典型应用。它们确保动态分配的内存即使在异常发生时也能被正确释放。
4. 异常处理的高级技巧
4.1 异常传播与重新抛出
有时我们需要在捕获异常后执行一些清理操作,然后让异常继续传播:
cpp复制try {
// ...
} catch (const std::exception& e) {
logError(e); // 记录错误
throw; // 重新抛出当前异常
}
注意throw;(不带参数)会重新抛出当前异常,保持原始异常类型和调用栈信息。而throw e;则会创建一个新的异常对象,可能丢失重要信息。
4.2 noexcept关键字与移动语义
C++11引入了noexcept关键字,用于标记函数不会抛出异常。这对于移动构造函数和移动赋值运算符特别重要:
cpp复制class MyType {
public:
MyType(MyType&& other) noexcept {
// 移动资源
}
MyType& operator=(MyType&& other) noexcept {
// 移动赋值
return *this;
}
};
标准库中的许多优化(如std::vector的重新分配)依赖于移动操作是noexcept的。如果移动操作可能抛出异常,标准库会回退到较慢的拷贝操作。
4.3 异常与多线程
在多线程环境中,异常不能跨线程传播。如果一个线程抛出的异常没有被捕获,程序会调用std::terminate。因此,每个线程都应该有自己的异常处理:
cpp复制void threadFunction() {
try {
// 线程工作代码
} catch (const std::exception& e) {
// 处理异常
}
}
std::thread t(threadFunction);
// ...
t.join();
对于异步任务,可以考虑使用std::future,它可以在主线程中获取子线程抛出的异常:
cpp复制auto future = std::async(std::launch::async, [] {
throw std::runtime_error("Error in async task");
return 42;
});
try {
int result = future.get(); // 这里会抛出异常
} catch (const std::exception& e) {
std::cerr << "Async task failed: " << e.what() << std::endl;
}
5. 工程实践中的异常处理策略
5.1 何时使用异常
异常最适合处理那些不常见、不可预测的错误情况。典型场景包括:
- 资源获取失败(文件、网络、内存等)
- 无效的用户输入(在无法立即验证的情况下)
- 违反类不变量的情况
相比之下,那些可以预见的、常规的错误(如用户登录失败)更适合使用错误码或bool返回值。
5.2 异常与性能
关于异常处理的性能,有几个关键点:
- 正常执行路径:没有异常抛出时,现代编译器的异常处理机制几乎零开销
- 抛出异常时:确实有较大开销(需要展开调用栈、查找处理程序等)
- 错误码检查:每次调用后检查错误码也有开销
因此,异常最适合那些"不应该发生"的错误情况。对于频繁发生的、可预期的错误,错误码可能更高效。
5.3 异常安全的设计模式
-
事务性操作:先执行所有可能失败的操作,最后提交更改
cpp复制void transferMoney(Account& from, Account& to, double amount) { double newFrom = from.balance() - amount; double newTo = to.balance() + amount; // 验证 if (newFrom < 0) throw InsufficientFunds(); // 原子性更新 from.setBalance(newFrom); to.setBalance(newTo); } -
写时复制(Copy-on-Write):修改前先创建副本,成功后再替换原对象
cpp复制void appendData(std::vector<int>& vec, int value) { std::vector<int> newVec = vec; // 复制 newVec.push_back(value); // 修改副本 vec.swap(newVec); // 原子性替换 } -
Pimpl惯用法:将实现细节隐藏在指针后面,使主要操作不抛出异常
cpp复制class Widget { struct Impl; std::unique_ptr<Impl> pImpl; public: void doSomething() noexcept { // 不抛出异常 // 通过pImpl完成实际工作 } };
6. 常见陷阱与最佳实践
6.1 异常处理的反模式
-
在析构函数中抛出异常:如果析构函数因栈展开而调用,此时再抛出异常会导致程序终止
cpp复制~MyClass() { // 危险:可能抛出异常 if (cleanupFailed()) throw std::runtime_error("Cleanup failed"); } -
吞掉异常:捕获异常后不做任何处理,这会掩盖严重问题
cpp复制try { riskyOperation(); } catch (...) { // 糟糕:完全忽略异常 } -
过度使用异常:将异常用于常规控制流,这会使代码难以理解和维护
cpp复制// 不好的做法:用异常代替简单的条件检查 try { throwIfInvalid(input); } catch (const ValidationError&) { handleInvalidInput(); }
6.2 异常安全的最佳实践
- 遵循RAII原则:用对象管理资源,确保异常安全
- 保持异常中立:除非明确要处理异常,否则应该让异常传播
- 提供强异常保证:关键操作要么完全成功,要么完全失败
- 记录异常:捕获异常时记录足够的信息以便调试
- 定义清晰的异常层次:自定义异常应该继承
std::exception并提供有意义的错误信息
6.3 异常处理检查清单
在代码审查时,我通常会检查以下异常安全问题:
- [ ] 所有资源获取是否使用RAII管理?
- [ ] 析构函数是否保证不抛出异常?
- [ ] 移动操作是否标记为noexcept?
- [ ] 关键操作是否提供适当的异常保证?
- [ ] 异常是否在适当层级被捕获和处理?
- [ ] 是否有异常被不恰当地吞掉?
- [ ] 自定义异常是否提供足够的信息?
7. 现代C++中的异常处理演进
7.1 C++17的异常处理改进
C++17引入了std::terminate_handler的改进,允许获取未捕获异常的更多信息:
cpp复制std::set_terminate([]() {
try {
std::rethrow_exception(std::current_exception());
} catch (const std::exception& e) {
std::cerr << "Uncaught exception: " << e.what() << std::endl;
} catch (...) {
std::cerr << "Unknown exception" << std::endl;
}
std::abort();
});
7.2 C++20的契约与异常
C++20虽然移除了契约(Contracts)特性,但相关讨论仍在继续。契约提供了一种声明式的方式来指定前置条件、后置条件和断言,可以与异常处理互补:
cpp复制// 概念性代码(非实际C++20语法)
void process(int* ptr)
[[pre: ptr != nullptr]] // 前置条件
[[post: *ptr == 42]] // 后置条件
{
*ptr = 42;
}
7.3 异常与协程
C++20引入的协程为异常处理带来了新的挑战和机会。协程可以在挂起时传播异常:
cpp复制task<void> asyncOperation() {
try {
co_await someAsyncTask();
} catch (const std::exception& e) {
// 处理异步异常
}
}
8. 跨语言边界的异常处理
8.1 C++与C的交互
C语言没有异常机制,所以C++异常不能跨越C函数边界传播。在与C代码交互时:
- 使用
extern "C"标记的函数不应该抛出异常 - 在调用C函数前保存所有可能抛出异常的对象状态
- 考虑使用错误码作为C/C++边界接口
cpp复制extern "C" int c_function() noexcept {
try {
// 调用可能抛出异常的C++代码
return cpp_function_wrapper();
} catch (...) {
return -1; // 转换为错误码
}
}
8.2 与其他语言的互操作
当C++作为库被其他语言(如Python、Java)调用时,需要将C++异常转换为目标语言的异常机制。以Python为例:
cpp复制// 使用Python C API包装C++函数
PyObject* wrapped_function(PyObject* self, PyObject* args) {
try {
// 调用实际的C++函数
cpp_function();
Py_RETURN_NONE;
} catch (const std::exception& e) {
PyErr_SetString(PyExc_RuntimeError, e.what());
return nullptr;
} catch (...) {
PyErr_SetString(PyExc_RuntimeError, "Unknown C++ exception");
return nullptr;
}
}
9. 性能分析与优化
9.1 异常处理的成本分析
异常处理机制的主要开销来自:
- 空间开销:异常处理表增加了二进制文件大小
- 时间开销:抛出异常时的栈展开和异常处理查找
- 优化限制:编译器对可能抛出异常的函数优化更保守
9.2 测量异常处理开销
可以使用简单的基准测试来比较异常与错误码的性能:
cpp复制#include <benchmark/benchmark.h>
bool withErrorCode(bool fail) {
if (fail) return false;
return true;
}
void withException(bool fail) {
if (fail) throw std::runtime_error("error");
}
static void BM_ErrorCode(benchmark::State& state) {
for (auto _ : state) {
benchmark::DoNotOptimize(withErrorCode(state.range(0)));
}
}
static void BM_Exception(benchmark::State& state) {
for (auto _ : state) {
try {
benchmark::DoNotOptimize(withException(state.range(0)));
} catch (...) {
}
}
}
BENCHMARK(BM_ErrorCode)->Arg(0)->Arg(1);
BENCHMARK(BM_Exception)->Arg(0)->Arg(1);
9.3 异常处理的优化技巧
- 热路径避免异常:在性能关键路径上,预先检查条件而不是依赖异常
- 使用noexcept:标记不会抛出异常的函数,帮助编译器优化
- 减小try块范围:只包围真正可能抛出异常的代码
- 避免异常类型过多:减少异常类型数量可以减小异常处理表
10. 大型项目中的异常处理策略
10.1 异常规范与风格指南
大型项目应该制定明确的异常处理规范,包括:
- 哪些情况应该使用异常
- 异常类的层次结构设计
- 异常安全保证的要求
- 异常传播的边界(如模块接口、线程边界)
Google的C++风格指南就明确禁止使用异常,而大多数其他大型项目(如LLVM)则允许谨慎使用异常。
10.2 异常安全的重构技巧
将旧代码改造为异常安全时,可以采用渐进式策略:
- 首先识别所有资源管理点,应用RAII
- 然后分析关键操作的异常安全保证
- 最后处理异常传播边界
例如,将裸指针转换为智能指针:
cpp复制// 改造前
void oldCode() {
Resource* res = new Resource();
// ...可能抛出异常的代码...
delete res;
}
// 改造后
void newCode() {
auto res = std::make_unique<Resource>();
// ...可能抛出异常的代码...
// res会自动释放
}
10.3 异常处理与单元测试
良好的异常处理应该被充分测试:
- 测试正常流程
- 测试各种错误条件下的异常抛出
- 测试异常处理逻辑
- 测试资源清理的正确性
使用测试框架如Google Test:
cpp复制TEST(ExceptionTest, ThrowsOnInvalidInput) {
EXPECT_THROW({
processInput(-1); // 无效输入应该抛出
}, std::invalid_argument);
}
TEST(ExceptionTest, NoThrowOnValidInput) {
EXPECT_NO_THROW({
processInput(42); // 有效输入不应该抛出
});
}
11. 自定义异常设计模式
11.1 异常类设计原则
良好的自定义异常应该:
- 继承自
std::exception或其派生类 - 提供有意义的错误信息
- 支持嵌套异常(C++11引入)
- 包含足够的上下文信息
cpp复制class DatabaseException : public std::runtime_error {
std::string query_;
int errorCode_;
public:
DatabaseException(const std::string& msg,
const std::string& query,
int errorCode)
: std::runtime_error(msg), query_(query), errorCode_(errorCode) {}
const std::string& query() const { return query_; }
int errorCode() const { return errorCode_; }
};
11.2 嵌套异常模式
C++11引入了std::nested_exception,允许异常链式传播:
cpp复制void handleHighLevel() {
try {
lowLevelOperation();
} catch (...) {
std::throw_with_nested(
std::runtime_error("High-level operation failed"));
}
}
void printException(const std::exception& e, int level = 0) {
std::cerr << std::string(level, ' ') << e.what() << '\n';
try {
std::rethrow_if_nested(e);
} catch (const std::exception& nested) {
printException(nested, level + 1);
}
}
11.3 领域特定异常
针对特定领域设计专门的异常类,例如网络编程:
cpp复制class NetworkException : public std::runtime_error {
public:
enum class ErrorType {
ConnectionFailed,
Timeout,
ProtocolError
};
NetworkException(ErrorType type, const std::string& details)
: std::runtime_error(makeMessage(type, details)),
type_(type), details_(details) {}
ErrorType type() const { return type_; }
const std::string& details() const { return details_; }
private:
ErrorType type_;
std::string details_;
static std::string makeMessage(ErrorType type, const std::string& details) {
// 生成完整的错误消息
}
};
12. 异常处理的可视化调试
12.1 调用栈分析
当异常被捕获时,获取完整的调用栈信息对调试至关重要。虽然C++标准没有提供调用栈API,但各平台有特定实现:
cpp复制#include <execinfo.h> // Linux
void printStackTrace() {
void* array[50];
size_t size = backtrace(array, 50);
char** symbols = backtrace_symbols(array, size);
for (size_t i = 0; i < size; ++i) {
std::cerr << symbols[i] << std::endl;
}
free(symbols);
}
// 在catch块中调用
catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
printStackTrace();
}
12.2 异常断点设置
现代调试器(如GDB、LLDB)支持设置异常断点:
bash复制# GDB中捕获所有异常抛出
catch throw
# 捕获特定类型异常
catch throw std::runtime_error
12.3 核心转储分析
配置程序在未捕获异常时生成核心转储:
bash复制ulimit -c unlimited # 允许生成核心转储
./your_program # 当崩溃时会产生core文件
gdb ./your_program core # 分析核心转储
在GDB中,可以使用bt命令查看异常抛出时的调用栈。
13. 异常处理与模板元编程
13.1 异常安全的通用代码
编写模板代码时,需要考虑类型参数可能抛出的异常:
cpp复制template <typename T>
void safeSwap(T& a, T& b) noexcept(noexcept(a.swap(b))) {
try {
a.swap(b);
} catch (...) {
// 回滚策略
std::terminate(); // 或者更优雅的处理
}
}
13.2 SFINAE与异常规范
可以利用异常规范进行SFINAE(替换失败不是错误):
cpp复制template <typename T>
auto callMaybeNoexcept(T& t) -> decltype(t.noexceptMethod(), std::true_type{}) {
t.noexceptMethod();
return std::true_type{};
}
template <typename T>
auto callMaybeNoexcept(T& t) -> decltype(t.method(), std::false_type{}) {
t.method();
return std::false_type{};
}
13.3 异常与constexpr
C++20开始,某些异常处理可以在编译期进行:
cpp复制constexpr int safeDivide(int a, int b) {
if (b == 0) throw "Division by zero"; // C++20允许
return a / b;
}
int main() {
constexpr int x = safeDivide(10, 2); // 编译期计算
// constexpr int y = safeDivide(10, 0); // 编译错误
}
14. 异常处理的最佳工具链
14.1 静态分析工具
使用静态分析工具检测异常安全问题:
- Clang-Tidy:检查异常安全、noexcept使用等
- Cppcheck:检测潜在的异常安全问题
- Coverity:商业工具,提供深入的异常安全分析
14.2 动态分析工具
运行时工具帮助诊断异常处理问题:
- Valgrind:检测异常导致的资源泄漏
- ASan/MSan:发现异常路径上的内存错误
- GDB/LLDB:调试异常处理流程
14.3 代码覆盖率
确保异常处理代码被充分测试:
- gcov/lcov:生成代码覆盖率报告
- LLVM Coverage:更精确的覆盖率分析
- 测试框架集成:如Google Test的覆盖率支持
15. 未来发展趋势与替代方案
15.1 Herbceptions提案
Herb Sutter提出的"Herbceptions"旨在改进C++异常机制:
- 零开销的正常执行路径
- 更简单的异常传播模型
- 更好的类型安全
cpp复制// 概念性代码(非实际C++语法)
int foo() throws {
if (failure) throw std::runtime_error("error");
return 42;
}
int bar() {
try {
return foo();
} catch (const std::runtime_error& e) {
return -1;
}
}
15.2 异常与协程的深度集成
未来C++版本可能进一步改进异常在协程中的传播:
cpp复制generator<int> coroutineWithException() {
try {
co_yield 42;
throw std::runtime_error("error");
} catch (...) {
// 协程内部的异常处理
}
}
15.3 其他错误处理机制
除了异常,现代C++还有其他错误处理方式:
- std::optional:表示可能有值的容器
- std::expected(C++23):包含值或错误信息
- 错误码+系统错误:
std::error_code和std::error_category
cpp复制std::expected<int, std::string> safeDivide(int a, int b) {
if (b == 0) return std::unexpected("Division by zero");
return a / b;
}
auto result = safeDivide(10, 0);
if (!result) {
std::cerr << "Error: " << result.error() << std::endl;
}
16. 实战:设计异常安全的容器类
让我们通过设计一个简单的动态数组来实践异常安全:
cpp复制template <typename T>
class SimpleVector {
T* data_ = nullptr;
size_t size_ = 0;
size_t capacity_ = 0;
void reserve(size_t new_capacity) {
if (new_capacity <= capacity_) return;
T* new_data = static_cast<T*>(operator new(new_capacity * sizeof(T)));
size_t i = 0;
try {
for (; i < size_; ++i) {
new (new_data + i) T(data_[i]); // 复制构造
}
} catch (...) {
for (size_t j = 0; j < i; ++j) {
new_data[j].~T(); // 销毁已构造的元素
}
operator delete(new_data);
throw; // 重新抛出
}
for (size_t j = 0; j < size_; ++j) {
data_[j].~T(); // 销毁旧元素
}
operator delete(data_);
data_ = new_data;
capacity_ = new_capacity;
}
public:
~SimpleVector() {
clear();
operator delete(data_);
}
void push_back(const T& value) {
if (size_ >= capacity_) {
reserve(capacity_ ? capacity_ * 2 : 1);
}
new (data_ + size_) T(value); // 复制构造
++size_;
}
void clear() noexcept {
for (size_t i = 0; i < size_; ++i) {
data_[i].~T();
}
size_ = 0;
}
// 其他成员函数...
};
这个实现展示了:
- 强异常保证:
reserve要么成功,要么完全回滚 - RAII管理:内存和对象生命周期正确管理
- noexcept清理:析构函数和
clear保证不抛出
17. 异常处理与多态设计
17.1 虚函数中的异常规范
设计多态接口时,需要考虑派生类可能抛出的异常:
cpp复制class Base {
public:
virtual ~Base() = default;
// 可能抛出任何异常
virtual void doSomething() = 0;
// 不抛出异常
virtual void noThrowMethod() noexcept = 0;
// 可能只抛出特定异常
virtual void specificThrow() throw(std::runtime_error) = 0;
};
注意:动态异常规范(
throw(type))在C++11后已弃用,应使用noexcept替代。
17.2 异常安全的多态赋值
实现多态类的赋值操作需要考虑异常安全:
cpp复制class Polymorphic {
// 实现细节...
public:
// 拷贝赋值提供强异常保证
Polymorphic& operator=(const Polymorphic& other) {
if (this != &other) {
auto temp = other.clone(); // 先创建副本
swap(*this, *temp); // 然后交换(不抛出)
}
return *this;
}
// 移动赋值标记为noexcept
Polymorphic& operator=(Polymorphic&& other) noexcept {
swap(*this, other);
return *this;
}
};
17.3 工厂模式与异常
工厂方法需要妥善处理对象创建时的异常:
cpp复制class ObjectFactory {
public:
std::unique_ptr<Base> create(const std::string& type) {
try {
if (type == "Derived1") return std::make_unique<Derived1>();
if (type == "Derived2") return std::make_unique<Derived2>();
throw std::invalid_argument("Unknown type");
} catch (const std::bad_alloc&) {
// 内存不足的特殊处理
return nullptr;
}
}
};
18. 异常处理与标准库
18.1 标准库异常安全保证
C++标准库为其操作提供明确的异常安全保证:
- 容器操作:通常提供基本保证,某些操作(如
std::vector::push_back)提供强保证 - 算法:大多数不抛出异常,除非比较操作或元素操作抛出
- 智能指针:所有操作至少提供基本保证,大多数不抛出
18.2 标准异常类体系
标准库定义了一套异常类层次结构:
code复制std::exception
├── std::bad_alloc
├── std::bad_cast
├── std::bad_typeid
├── std::logic_error
│ ├── std::invalid_argument
│ ├── std::domain_error
│ ├── std::length_error
│ └── std::out_of_range
└── std::runtime_error
├── std::range_error
├── std::overflow_error
├── std::underflow_error
├── std::system_error
└── std::ios_base::failure
18.3 标准库中的noexcept函数
许多标准库函数标记为noexcept,包括:
- 所有移动构造函数和移动赋值运算符
- 基本类型操作
- 智能指针操作(除
std::shared_ptr的原子操作) - 大多数标准算法
19. 异常处理与并发编程
19.1 异步操作中的异常
处理异步操作(如std::async)的异常:
cpp复制auto future = std::async(std::launch::async, [] {
throw std::runtime_error("Async error");
return 42;
});
try {
int result = future.get(); // 异常在此处抛出
} catch (const std::exception& e) {
std::cerr << "Async error: " << e.what() << std::endl;
}
19.2 并行算法异常
C++17并行算法中的异常处理:
cpp复制try {
std::vector<int> data = /*...*/;
std::for_each(std::execution::par, data.begin(), data.end(), [](int& x) {
if (x < 0) throw std::invalid_argument("Negative value");
x = process(x);
});
} catch (const std::exception& e) {
// 任一工作线程抛出异常都会传播到这里
}
19.3 原子操作与异常
原子操作通常不抛出异常,但需要注意:
cpp复制std::atomic<int> counter{0};
void increment() noexcept {
try {
++counter; // 不会抛出
} catch (...) {
// 永远不会执行
}
}
20. 异常处理与资源管理
20.1 文件与网络资源
管理文件系统操作中的异常:
cpp复制void processFile(const std::string& filename) {
std::ifstream file(filename);
if (!file) throw std::runtime_error("File open failed");
try {
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
// 处理内容...
} catch (...) {
file.close();
throw; // 重新抛出
}
}
20.2 数据库连接
数据库操作的异常安全处理:
cpp复制class DatabaseConnection {
sqlite3* db;
public:
explicit DatabaseConnection(const char* filename) : db(nullptr) {
if (sqlite3_open(filename, &db) != SQLITE_OK) {
throw DatabaseException(sqlite3_errmsg(db));
}
}
~DatabaseConnection() {
if (db) sqlite3_close(db);
}
// 禁用拷贝
DatabaseConnection(const DatabaseConnection&) = delete;
DatabaseConnection& operator=(const DatabaseConnection&) = delete;
};
20.3 锁与异常安全
确保异常发生时锁被正确释放:
cpp复制std::mutex mtx;
void criticalSection() {
std::unique_lock<std::mutex> lock(mtx);
// 临界区代码(可能抛出异常)
// 锁会在作用域结束时自动释放
}
21. 异常处理与性能关键系统
21.1 禁用异常的情况
在某些性能关键系统中,可能完全禁用异常:
- 嵌入式系统(资源受限)
- 高频交易系统(需要确定性的性能)
- 实时系统(不能接受异常处理的不可预测性)
使用编译选项-fno-exceptions(GCC/Clang)或/EHs-c-(MSVC)禁用异常。
21.2 替代方案设计
禁用异常时的错误处理策略:
- 错误码:函数返回错误状态
- 断言:开发时检查,发布时禁用
- 终止:不可恢复错误直接终止
- 双返回模式:返回值和错误状态
cpp复制enum class Error { None, InvalidInput, ResourceExhausted };
std::pair<int, Error> safeOperation(int input) {
if (input < 0) return {0, Error::InvalidInput};
return {input * 2, Error::None};
}
21.3 异常与低延迟编程
低延迟系统的异常处理技巧:
- 预分配所有资源
- 使用对象池避免动态分配
- 将可能失败的操作提前到初始化阶段
- 使用快速路径/慢速路径分离
cpp复制class LowLatencyProcessor {
std::vector<WorkItem> preallocatedItems;
public:
LowLatencyProcessor(size_t capacity)
: preallocatedItems(capacity) {} // 预分配
void process(const Input& input) noexcept {
try {
// 实际处理(异常不会逃逸)
internalProcess(input);
} catch (...) {
logError(); // 记录但不传播
}
}
};
22. 异常处理与测试驱动开发
22.1 测试异常抛出
使用测试框架验证异常行为:
cpp复制TEST(Exceptions, DivisionByZero) {
EXPECT_THROW(divide(10, 0), std::invalid_argument);
EXPECT_NO_THROW(divide(10, 2));
EXPECT_ANY_THROW(riskyOperation());
}
22.2 模拟异常场景
在单元测试中模拟异常条件:
cpp复制class MockDatabase : public DatabaseInterface {
public:
MOCK_METHOD(bool, connect, (), (override));
MOCK_METHOD(Result, query, (const std::string&), (override));
};
TEST(DatabaseTest, HandlesConnectionFailure) {
MockDatabase db;
EXPECT_CALL(db, connect()).WillOnce(Throw(DatabaseException("Connection failed")));
DatabaseClient client(&db);
EXPECT_THROW(client.initialize(), DatabaseException);
}
22.3 异常安全测试
验证代码的异常安全保证:
cpp复制TEST(VectorTest, StrongExceptionGuaranteeOnPushBack) {
std::vector<ThrowingType> vec;
ThrowingType::setThrowAfter(3); // 第4次构造抛出
try {
vec.push_back(ThrowingType{}); // 应该回滚
FAIL() << "Expected exception";
} catch (...) {
EXPECT_TRUE(vec.empty()); // 强保证:操作完全回滚
}
}