本章目标:理解异常安全的四个等级、RAII 的深层机制、 强异常保证、copy-and-swap 惯用法、noexcept 的正确用法。 这是"库作者级别"的品质要求,普通代码也能受益。
异常可能在任何一行抛出(内存不足、IO 错误、用户代码)。 如果函数中途抛异常:
void transfer(BankAccount& a, BankAccount& b, double amt) {
a.withdraw(amt); // 先扣钱
b.deposit(amt); // 如果这里抛异常 → 钱丢了!
}
这就是"异常不安全":抛异常后数据处于不一致状态。 银行的代码绝对不能这样写。
从弱到强,函数应该尽量做更强的承诺:
抛异常时:资源不泄漏,对象状态**合法但可能被部分修改** —— 大多数函数都在这个级别
抛异常时:**要么全部成功,要么完全没发生**(事务语义)
绝不抛异常(noexcept 声明)——析构、移动操作必须
人话:写函数前问自己——"它抛异常时,世界的状态是什么?"
把"可能失败的操作"全部前置,成功后一次性提交:
void transfer(BankAccount& a, BankAccount& b, double amt) {
a.withdraw(amt); // 第一处修改:可能失败?
b.deposit(amt); // 第二处修改:可能失败!
}
改为:
void transfer_safe(BankAccount& a, BankAccount& b, double amt) {
if (a.balance() < amt)
throw std::runtime_error("余额不足"); // 先验证
if (!b.can_accept(amt))
throw std::runtime_error("接收失败"); // 全部验证完
// 两处修改都不会失败(noexcept 操作)→ 强保证
a.withdraw(amt);
b.deposit(amt);
}
"先验证后提交" = 事务思维。
给类写"operator="时,最优雅的强保证写法:
class Widget {
std::vector<int> data_;
public:
Widget() = default;
Widget(const Widget&) = default; // 拷贝构造
Widget& operator=(Widget other) { // 按值传参!
swap(other); // 与临时副本交换
return *this;
} // other 出作用域销毁旧数据(自动释放)
void swap(Widget& o) noexcept {
data_.swap(o.data_);
}
};
原理:
原对象纹丝不动(强保证)
一条规则:operator= 用 copy-and-swap,自动获得强保证 + 异常安全。
RAII 本质:把"资源生命周期"绑定到"对象生命周期"。 资源包括:内存、文件、锁、句柄、网络连接、数据库事务。
// 锁(已见过)
std::lock_guard<std::mutex> l(mtx);
// 文件
std::ofstream f("x.txt");
// 智能指针
std::unique_ptr<T> p = std::make_unique<T>();
// 事务(自写)
class Transaction {
public:
explicit Transaction(Db& db) : db_(db) { db_.begin(); }
~Transaction() {
if (committed_) db_.commit();
else db_.rollback();
}
void commit() { committed_ = true; }
private:
Db& db_;
bool committed_ = false;
};
void do_work(Db& db) {
Transaction tx(db); // 构造 = 开始事务
db.insert(...);
// 忘记 commit()?析构自动回滚!异常?同样回滚!
}
自己写 RAII 类的原则:
栈展开期间如果析构函数再抛异常:
~MyClass() { // 隐含 noexcept
cleanup(); // 如果 cleanup 抛异常 → terminate
}
处理:析构里捕获所有异常,吞掉或记录:
~MyClass() {
try { cleanup(); }
catch (...) { /* 记录日志,但绝不再抛出 */ }
}
标记 noexcept 的函数:
该标记的:
不该标记的:
void f() noexcept { ... } // 内部调用了会抛异常的函数
// 一旦真抛 → terminate,而不是向上传播!
经验:宁可少标,不可错标。
(代码里没有 try/catch 时完全不引入额外开销)
大约微秒级——每秒几千次是性能问题,每秒几次无所谓
结论:正常业务代码用异常没问题; 实时/极高性能路径用 expected(零异常路径开销更稳)。
写一个支持强保证的栈容器(完整版见测试):
template <typename T>
class SafeStack {
std::vector<T> data_;
public:
void push(const T& v) {
data_.push_back(v); // 若失败:vector 自身保持原状态
} // (vector 提供强保证)
void pop() {
if (data_.empty()) throw std::runtime_error("空栈");
data_.pop_back();
}
// 移动构造 noexcept
SafeStack(SafeStack&& o) noexcept = default;
};
标准库容器的保证:
(资源放成员里(RAII)就安全;放裸指针里就泄漏!)
练习题(配套测试:测试_第23章_异常安全.cpp)