条款29:为 “异常安全” 而努力是值得的
当异常被抛出时,带有异常安全性的函数会:
- 不泄漏任何资源。
- 不允许数据败坏。
异常安全函数提供以下三个基本保证:
- 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而破坏。
void basicOperation() { std::string s; try { s = "hello"; // 如果此处发生异常,s 依然有效,状态未损坏。 } catch (...) { // 异常处理,确保资源安全 } }
- 强烈保证:如果异常被抛出,程序状态不改变。
class Widget { public: void setName(const std::string& newName) { std::string temp(newName); // 创建副本 swap(*this, name_); // 确保异常时状态回滚 } private: void swap(Widget& lhs, Widget& rhs) noexcept { std::swap(lhs.data_, rhs.data_); // 假设 std::swap 不抛异常 } std::string name_; };
- 不抛出异常保证:承诺绝不抛出异常。
void nothrowFunction() noexcept { // 不可能抛出异常的代码 }
实现强烈保证的技巧
Copy-and-Swap 技术是一种实现强烈保证的常见方式:
- 创建副本并在副本上操作。
- 使用异常安全的
swap
替换原始对象。
class Widget { public: Widget& operator=(Widget rhs) { // rhs 是副本 swap(*this, rhs); // 保证强烈保证 return *this; } private: void swap(Widget& lhs, Widget& rhs) noexcept { std::swap(lhs.data_, rhs.data_); // 假设 std::swap 不抛异常 } std::string data_; };
选择适当的异常安全保证
- 基本承诺 是最低要求:在任何情况下,程序都必须保持有效状态。
- 强烈保证:如果提供强烈保证可显著提升代码安全性和可读性,且付出的成本合理,应该努力实现。
- 不抛异常保证:对于某些特定函数(如析构函数、swap 函数),必须提供不抛异常保证。
异常安全的实现原则
- RAII
将资源管理责任交给 RAII 对象,确保资源释放无论何时都能正确发生。
void safeFunction() { std::unique_ptr<Widget> w(new Widget); // 确保异常时资源释放 // 其他逻辑 }
-
事务性操作
- 函数中的任何修改都以事务形式完成,确保中途失败不影响原始状态。
-
封装异常处理逻辑
避免让调用者直接处理异常,将异常安全性封装在接口中。 -
尽量避免动态分配资源
优先使用标准容器(如std::vector
)和智能指针来管理资源。
注意事项
-
异常安全保证的传递性
函数提供的异常安全性,通常等于其调用的所有子函数中最弱的异常安全保证。 -
成本与收益权衡
并非所有函数都能或需要提供强烈保证。为一些复杂或高性能函数,基本保证可能更实际。 -
析构函数必须 nothrow
确保资源释放阶段不会抛出异常。
总结
- 目标:努力使函数具有异常安全性,保护资源完整性和逻辑一致性。
- 层次:提供基本承诺、强烈保证或不抛异常保证,视情况而定。
- 技术:通过 RAII、copy-and-swap 和事务性操作实现异常安全。
- 权衡:在性能、复杂性和安全性之间找到平衡点。
为异常安全而努力,不仅能提高代码健壮性,也能显著减少调试和维护的成本。