More Effective C++条款12:理解抛出一个异常与传递一个参数或调用一个虚函数间的差异
More Effective C++ 条款12:理解"抛出一个异常"与"传递一个参数"或"调用一个虚函数"间的差异
核心思想
虽然从语法上看,抛出异常与传递函数参数相似,但它们在实现机制、执行效率和行为特性上存在本质差异。理解这些差异对于编写高效、正确的异常处理代码至关重要。
1. 关键差异分析
1.1 控制流差异
- 函数调用:控制权最终会返回到调用处
- 异常抛出:控制权永远不会回到抛出异常的地方
1.2 对象复制行为对比
特性 | 传递参数 | 抛出异常 |
---|---|---|
复制次数 | 0-1次(可能不复制) | 1-2次(总是至少复制一次) |
复制时机 | 可能延迟或优化掉 | 总是立即复制 |
复制类型 | 动态类型复制 | 静态类型复制 |
避免复制 | 使用引用/指针 | 使用引用捕获(但仍有一次复制) |
// 示例:异常对象总是被复制
class ExceptionObject {
public:ExceptionObject() { std::cout << "Constructor\n"; }ExceptionObject(const ExceptionObject&) { std::cout << "Copy constructor\n"; }
};void throwException() {ExceptionObject e; // 输出: Constructorthrow e; // 输出: Copy constructor(创建临时对象)
}void catchException() {try {throwException();} catch (ExceptionObject e) { // 输出: Copy constructor(复制到catch参数)// 处理异常}
}
// 总输出: Constructor → Copy constructor → Copy constructor
1.3 类型转换限制
- 参数传递:允许广泛的隐式类型转换
- 异常抛出:只允许有限的类型转换(继承类到基类、非常量到常量、数组/函数到指针)
// 有限的类型转换示例
class Base { /*...*/ };
class Derived : public Base { /*...*/ };void throwDerived() {Derived d;throw d; // 允许:Derived到Base的转换
}void catchBase() {try {throwDerived();} catch (const Base& b) { // 正确捕获// 处理异常}
}
1.4 捕获顺序与虚函数调用差异
- 异常捕获:按源代码顺序匹配第一个合适的catch子句
- 虚函数调用:选择与对象动态类型最匹配的函数
// 异常捕获顺序的重要性
try {// 可能抛出多种异常的代码
} catch (const std::exception& e) {// 捕获所有标准异常
} catch (const MySpecialException& e) {// 永远不会执行:因为std::exception更通用且在前
} catch (...) {// 捕获所有其他异常
}// 正确的顺序应该是从具体到通用:
try {// 可能抛出多种异常的代码
} catch (const MySpecialException& e) {// 处理特定异常
} catch (const std::exception& e) {// 处理标准异常
} catch (...) {// 处理未知异常
}
2. 效率与性能影响
2.1 异常处理的开销来源
- 对象复制:异常对象至少被复制一次
- 栈展开:需要遍历调用栈查找匹配的catch块
- 运行时支持:需要维护类型信息和调用栈元数据
2.2 优化建议
-
使用引用捕获异常:避免额外的对象复制
// ✅ 推荐:使用引用捕获异常 try {// 可能抛出异常的代码 } catch (const MyException& e) { // 只复制一次(抛出时)// 处理异常 }// ❌ 避免:使用值捕获异常 try {// 可能抛出异常的代码 } catch (MyException e) { // 复制两次:抛出时 + 捕获时// 处理异常 }
-
优先使用noexcept函数:明确标识不会抛出异常的函数
-
避免不必要的异常抛出:在性能关键路径中谨慎使用异常
3. 重新抛出异常的正确方式
// ✅ 正确:重新抛出当前异常(不创建新副本)
try {// 可能抛出异常的代码
} catch (const MyException& e) {// 部分处理...throw; // 重新抛出原始异常对象
}// ❌ 错误:创建新的异常副本
try {// 可能抛出异常的代码
} catch (const MyException& e) {// 部分处理...throw e; // 创建新的副本,丢失原始异常信息
}
4. 特殊情况下临时对象的处理
// 临时对象在异常处理中的行为
void functionThatThrows() {throw MyException(); // 创建临时对象并复制
}void testTemporary() {try {functionThatThrows();} catch (const MyException& e) { // 引用绑定到异常对象// 即使异常对象是临时对象,也允许非const引用捕获// 这是异常处理与函数参数传递的另一个差异}
}
5. 实践建议总结
- 总是通过引用捕获异常:避免不必要的对象复制
- 合理安排catch子句顺序:从最具体到最通用
- 使用
throw;
重新抛出异常:保持原始异常信息 - 了解异常处理的开销:在性能敏感代码中谨慎使用
- 利用RAII管理资源:确保异常安全
总结
条款12共同强调了C++异常处理的关键原则:安全性第一,效率第二。条款12则帮助开发者理解异常传递的内部机制以避免常见陷阱。掌握这两个条款对于编写健壮、高效的C++异常安全代码至关重要。
综合建议:
- 在析构函数中使用
noexcept
并彻底处理所有异常- 通过引用捕获异常以减少复制开销
- 使用RAII模式管理资源,确保异常安全
- 了解异常处理的开销,在性能关键代码中谨慎使用异常
- 安排catch子句顺序从具体到通用