一句话核心结论 :C++ 异常的下半部分讲 3 个工程细节——catch by reference 的 4 大优势(避免切片、避免拷贝、支持多态、避免重新抛出对象问题)、异常规格 的演进(throw() → noexcept)、异常处理的真实成本 (”零成本”模型 + try 块的代码膨胀)。
系列导航 前言:异常处理的”最后一块拼图” 第 3 篇讲了异常的”4 大支柱”(RAII、构造期安全、析构不抛、catch by ref)。
本篇深入 3 个工程细节 ——
catch by reference 为什么是”最优”?异常规格 throw(T1, T2) 为什么被废弃?try/catch 的真实成本 是什么?graph TB
A["异常处理\n3 大工程细节"] --> B["catch by ref\n(条款 13)"]
A --> C["异常规格\n(条款 14)"]
A --> D["异常成本\n(条款 15)"]
B -.->|4 大优势| B1["避免切片\n避免拷贝\n支持多态\n避免 rethrow"]
C -.->|C++11| C1["throw() → noexcept"]
D -.->|零成本| D1["无异常时几乎无开销\n有异常时才付出"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFB3C6,stroke:#F48FB1,color:#333
style B1 fill:#FFF9C4,stroke:#F9A825,color:#333
style C1 fill:#FFF9C4,stroke:#F9A825,color:#333
style D1 fill:#FFF9C4,stroke:#F9A825,color:#333一、条款 13:以 by reference 方式捕捉 exceptions 1.1 3 种 catch 方式对比 1 2 3 4 5 6 7 8 9 try { } catch (Widget w) { } catch (Widget& w) { } catch (Widget* w) { }
1.2 by value 的 3 大问题 问题 1:切片 1 2 3 4 5 6 7 8 class Base { public : virtual void foo () ; };class Derived : public Base { public : int x; };try { throw Derived (); } catch (Base b) { b.foo (); }
问题 :
catch (Base b) 按值接收——构造一个新 Base 对象派生部分被”切掉” 多态丢失 问题 2:拷贝 2 次 1 2 3 4 5 try { throw Widget (); } catch (Widget w) { }
问题 3:重新抛出时可能拷贝 1 2 3 4 5 6 try { } catch (Widget w) { throw ; }
1.3 by reference 的 4 大优势 1 2 3 4 5 6 7 8 9 try { } catch (const Widget& w) { }
4 大优势 :
优势 说明 避免切片 接收的是原对象,不是拷贝 避免拷贝 0 次拷贝(引用) 支持多态 调虚函数保留动态类型 rethrow 简单 throw; 直接传原对象
1.4 by pointer 的特殊情况 1 2 3 4 5 6 7 catch (Widget* w) { }
反例 :
1 2 3 4 5 6 7 8 9 void f () { Widget w; try { throw &w; } catch (Widget* p) { } }
1.5 实战:异常类的设计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class MyException : public std::exception { std::string msg_; int code_; public : MyException (const std::string& msg, int code) : msg_ (msg), code_ (code) {} const char * what () const noexcept override { return msg_.c_str (); } int code () const noexcept { return code_; } }; try { throw MyException ("oops" , 42 ); } catch (const std::exception& e) { std::cout << e.what () << "\n" ; }
1.6 关键启示 catch by reference ——首选catch by value ——除了最简单的异常类型,避免catch by pointer ——几乎不推荐const T& ——最佳:不可修改、避免拷贝、支持多态二、条款 14:明智运用 exception specifications 2.1 C++98 的异常规格 1 2 3 4 void f () throw (std::runtime_error) ; void g () throw () ; void h () ;
2.2 异常规格的问题 1 2 3 4 5 6 7 8 9 10 11 12 13 class Widget {public : void f () throw (int ) ; }; void Widget::f () { }
问题 :
异常规格是”承诺”——违反承诺 → std::unexpected → std::terminate 编译器生成的代码会”检查”——有运行时成本 维护困难——函数改了,规格忘了改 2.3 C++11 的解决方案:noexcept 1 2 3 4 void f () noexcept ; void g () noexcept (true ) ; void h () noexcept (false ) ;
C++11 的 noexcept 优势 :
维度 throw()noexcept表达 类型列表 bool 表达式 验证 编译期 + 运行期 编译期 违反后果 std::unexpectedstd::terminate优化 编译器保守 编译器激进(不抛 = 优化) 表达式 否 noexcept(expr)
2.4 noexcept 表达式 1 2 3 4 5 6 7 8 9 10 11 template <typename T>void swap (T& a, T& b) noexcept (std::is_nothrow_move_constructible_v<T>) ;template <typename T>class vector { void push_back (const T& x) ; void push_back (T&& x) noexcept (std::is_nothrow_move_constructible_v<T>) ; };
2.5 实战:什么时候用 noexcept? 1 2 3 4 5 6 7 8 9 10 11 12 class Widget { ~Widget () noexcept ; void swap (Widget& other) noexcept ; Widget (Widget&& other) noexcept ; }; class Database { void query () noexcept ; void connect () noexcept ; };
原则 :
不抛的函数 → noexcept可能抛的函数 → 不加 noexcept绝不要”为了 noexcept 而 noexcept” ——会 terminate2.6 noexcept 的”5 大好处” 1 2 3 4 5 6 7 void f () noexcept ; std::vector<T>::push_back (T&&) noexcept (...);
2.7 关键启示 noexcept 优于 throw() ——C++11 标准不抛的函数加 noexcept ——帮助编译器优化不要”为了 noexcept 而 noexcept” ——违反会 terminatenoexcept(expr) ——条件性 noexcept三、条款 15:了解异常处理的成本 3.1 零成本模型(Zero-Cost Model) 这是和 Java 的最大区别 :
Java 异常 :try/catch 块本身有开销C++ 异常 :try/catch 块几乎零成本,throw 时才付出3.2 零成本模型的实现 汇编层面 :
1 2 3 4 5 6 7 8 9 10 ; 无异常时 main: call foo ; 不抛异常 ; 继续 ; 有异常时 foo: call bar ; 假设 bar 抛异常 ; CPU 查 exception table ; 跳转到 catch 子句
3.3 try 块的真实成本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void f () { foo (); bar (); baz (); } void g () { try { foo (); bar (); baz (); } catch (...) { } }
汇编对比 :
f() 和 g() 的执行路径几乎相同g() 多一点栈空间(异常表)实际”额外开销”几乎为 0 3.4 真正”贵”的操作 1 2 3 4 5 6 7 8 9 10 11 void f () { if (error) { throw std::runtime_error ("..." ); } } catch (Widget w) { }
真实成本 :
操作 相对成本 正常函数调用 1x 抛异常 + 捕获 ~10-100x 异常对象拷贝 1-2x 栈展开(每层) 较小
3.5 “异常 vs 错误码” 的决策 1 2 3 4 5 6 7 8 9 10 11 12 13 void connect () { if (failed) throw std::runtime_error ("connect failed" ); } bool tryParseInt (const std::string& s, int & result) { if (failed) return false ; return true ; }
原则 :
异常 :错误”罕见”且”严重”错误码 :错误”常见”且”用户可恢复”3.6 实战:异常性能的 4 个建议 1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (i == 0 ) throw std::runtime_error ("zero" ); for (int i = 0 ; i < n; ++i) { if (i == 0 ) throw ; } throw std::string ("error" ); throw MyException (); void f () noexcept ;
3.7 关键启示 零成本模型 ——无异常时几乎零开销抛异常本身很贵 ——比函数调用慢 10-100 倍不要把异常当”控制流” ——性能杀手noexcept 帮编译器优化 ——但不要”为了 noexcept 而 noexcept”四、3 个条款的”异常(下)”全景 graph TB
A["异常(下)"] --> B["catch by ref\n(条款 13)"]
A --> C["异常规格\n(条款 14)"]
A --> D["异常成本\n(条款 15)"]
B --> B1["避免切片\n避免拷贝\n支持多态\nrethrow 简单"]
C --> C1["noexcept\n优于 throw()"]
D --> D1["零成本模型\n抛才付出"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFB3C6,stroke:#F48FB1,color:#333
style B1 fill:#FFF9C4,stroke:#F9A825,color:#333
style C1 fill:#FFF9C4,stroke:#F9A825,color:#333
style D1 fill:#FFF9C4,stroke:#F9A825,color:#333核心思路 :
catch by ref :4 大优势,避免切片 + 拷贝noexcept :C++11 优于 throw(),编译器优化零成本模型 :无异常时几乎零开销五、常见误区与陷阱 5.1 误区 1:catch by value 1 2 3 4 5 catch (Widget w) { }catch (const Widget& w) { }
5.2 误区 2:滥用 noexcept 1 2 3 4 5 6 7 8 9 void connect () noexcept { } void connect () { }
5.3 误区 3:用异常当控制流 1 2 3 4 5 6 7 8 9 for (int i = 0 ; i < 1000 ; ++i) { try { ... } catch (...) { ... } } for (int i = 0 ; i < 1000 ; ++i) { if (cond) { ... } }
5.4 误区 4:rethrow 时改对象 1 2 3 4 5 6 7 8 9 10 11 catch (Widget w) { w.modify (); throw ; } catch (Widget& w) { w.modify (); throw ; }
六、C++11/14/17/20 的演进 主题 C++98 时代 C++11/14/17/20 时代 异常规格 throw(T1, T2)noexcept / noexcept(expr)抛出意外异常 std::unexpectedstd::terminate异常对象 自定义 std::system_errorcurrent_exception无 std::current_exception()exception_ptr无 std::exception_ptrnested_exception无 std::nested_exception性能 “零成本” 同 + 更激进优化
C++17 的 std::uncaught_exceptions() :
1 2 3 4 5 6 7 8 9 10 11 12 class ScopeGuard { int uncaughtAtConstruction_; public : ScopeGuard () : uncaughtAtConstruction_ (std::uncaught_exceptions ()) {} ~ScopeGuard () { if (std::uncaught_exceptions () > uncaughtAtConstruction_) { } else { } } };
C++11 的 std::current_exception() :
1 2 3 4 5 6 7 try { } catch (...) { auto eptr = std::current_exception (); }
七、面试高频考点 7.1 必背题 题目 答案要点 catch by ref 的优势? 避免切片、避免拷贝、支持多态、rethrow 简单 noexcept vs throw()? noexcept 编译期 + 编译器优化 异常处理的成本? 零成本模型:无异常时几乎零开销 抛异常本身很贵吗? 是——比函数调用慢 10-100 倍 为什么不用异常当控制流? 性能杀手 noexcept 违反会怎样? std::terminate rethrow 怎么写? throw;(不带表达式)异常对象的大小? 看 catch by value 还是 by ref
7.2 高频追问 追问 关键点 catch (T) 切片? 是——按值构造新对象 noexcept 表达式的优势? 条件性 noexcept 异常表的实现? 类似 DWARF,函数入口+范围 std::uncaught_exceptions 干什么? 判断”析构是否在栈展开中” 跨线程传异常? std::exception_ptr异常性能 10-100x 慢在哪? 栈展开 + 异常对象构造 + catch 处理
八、配套实验 8.1 实验 1:catch 3 种方式对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <iostream> class Base {public : virtual ~Base () = default ; virtual void who () const { std::cout << "Base\n" ; } }; class Derived : public Base {public : void who () const override { std::cout << "Derived\n" ; } void special () const { std::cout << "Derived::special\n" ; } }; int main () { std::cout << "=== catch by value ===\n" ; try { throw Derived (); } catch (Base b) { b.who (); } std::cout << "\n=== catch by reference ===\n" ; try { throw Derived (); } catch (Base& b) { b.who (); } std::cout << "\n=== catch by const ref ===\n" ; try { throw Derived (); } catch (const Base& b) { b.who (); } return 0 ; }
8.2 实验 2:noexcept 表达 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <iostream> #include <type_traits> class Widget {public : ~Widget () noexcept = default ; Widget (Widget&&) = default ; void swap (Widget& other) noexcept { using std::swap; } }; template <typename T>void safeSwap (T& a, T& b) noexcept (std::is_nothrow_swappable_v<T>) { using std::swap; swap (a, b); } int main () { std::cout << "Widget nothrow swappable: " << std::is_nothrow_swappable_v<Widget> << "\n" ; int a = 1 , b = 2 ; safeSwap (a, b); std::cout << "a=" << a << " b=" << b << "\n" ; return 0 ; }
8.3 实验 3:异常成本实测 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <iostream> #include <chrono> int normalFunc () { return 42 ; } int exceptionFunc (int shouldThrow) { if (shouldThrow) { throw std::runtime_error ("oops" ); } return 42 ; } int main () { auto start = std::chrono::steady_clock::now (); for (int i = 0 ; i < 1000000 ; ++i) { int x = normalFunc (); (void )x; } auto end = std::chrono::steady_clock::now (); auto normalTime = std::chrono::duration_cast <std::chrono::microseconds>(end - start).count (); std::cout << "Normal: " << normalTime << " μs\n" ; start = std::chrono::steady_clock::now (); for (int i = 0 ; i < 1000000 ; ++i) { try { int x = exceptionFunc (0 ); (void )x; } catch (...) { } } end = std::chrono::steady_clock::now (); auto noExceptionTime = std::chrono::duration_cast <std::chrono::microseconds>(end - start).count (); std::cout << "Try block (no throw): " << noExceptionTime << " μs\n" ; return 0 ; }
8.4 实验 4:noexcept 违反 = terminate 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <iostream> #include <stdexcept> class NoThrow {public : ~NoThrow () noexcept (false ) { throw std::runtime_error ("dtor" ); } }; class SafeThrow {public : ~SafeThrow () noexcept { try { throw std::runtime_error ("dtor" ); } catch (...) { std::cout << "Caught in dtor\n" ; } } }; int main () { std::cout << "Test 1: NoThrow (will terminate)\n" ; try { } catch (...) { std::cout << "Won't reach\n" ; } std::cout << "\nTest 2: SafeThrow (OK)\n" ; SafeThrow s; return 0 ; }
九、回到 3 条黄金法则 条款 黄金法则 13 catch by const T&——避免切片 + 拷贝 + 多态 14 noexcept 优于 throw()——编译器优化15 异常是”零成本”——只在抛时付出
十、结尾思考题 思考题 1 :catch (Base b) 和 catch (const Base& b) 的差异是什么?多态 + 拷贝方面。
思考题 2 :实现一个 noexcept 标记的移动构造函数,验证其性能优势。
思考题 3 :C++ 异常处理是”零成本模型”——这意味着什么?和无异常时性能对比。
思考题 4 :rethrow; 和 rethrow e; 的差异是什么?哪个是惯用法?
思考题 5 :你的项目里有哪些”用异常当控制流”的地方?如何优化?
十一、本篇速查表 主题 关键 API / 模式 适用场景 catch by ref catch (const T& e)几乎所有 catch catch by value catch (T e)简单类型 + 不需要多态 noexcept void f() noexcept;不抛的函数 noexcept(expr) noexcept(is_nothrow_xxx<T>)条件性 noexcept 异常零成本 几乎零开销 日常编码 抛异常成本 ~10-100x 谨慎使用
十二、系列导航 # 文章 状态 0 系列总览 ✅ 已发布 1 基础议题 ✅ 已发布 2 操作符 ✅ 已发布 3 异常(上) ✅ 已发布 4 本文:异常(下) ✅ 已发布 5 效率(上):lazy evaluation、临时对象、RVO 🔜 计划中 6 效率(下):重载、operator new、内存池、inline 🔜 计划中 7 技术:虚拟构造、智能指针、引用计数、双重分派 🔜 计划中 8 杂项 + 总结:未来时态、标准库、命名空间 🔜 计划中
下一篇 :第 5 篇《效率(上):80-20 法则、lazy evaluation、临时对象、RVO》——条款 16-20 一起讲透 C++ 效率优化:80-20 法则的工程意义、lazy evaluation 的 4 大场景、分期摊还、临时对象、返回值优化 RVO。
行动建议 :
今天 :把所有 catch 子句改为 const T& 形式今天 :把不抛的析构、swap、移动构造标 noexcept本周 :识别你项目里的”用异常当控制流”——改用错误码本周 :用 noexcept(is_nothrow_xxx<T>) 标记模板函数思考 :你的项目里哪些函数应该标记 noexcept?