《Exceptional C++》— Herb Sutter
源自 Guru of the Week 专栏的 47 个 C++ 工程难题与解答,异常安全与现代 C++ 设计的经典之作。
基本信息
- 原名:Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions
- 作者:Herb Sutter(C++ 标准委员会核心成员)
- 中文版:侯捷 等合译(draft 版,民间译本)
- 出版社:Addison-Wesley(英文版 2000 年)
- 来源:Usenet 专栏 Guru of the Week(GotW)第 1~30 期及扩充内容
- 页数:约 80 页(draft 版节译)/ 全书约 300 页
- 难度定位:进阶-专家级,面向已熟练掌握 C++ 的程序员
核心定位
这本书不是入门教材,也不是语法参考书——它是一本软件工程实战训练手册。采用「问题/解答」格式,每个条款都以一个 C++ 编程难题开场,引导读者思考,再给出深度剖析。
Scott Meyers 评价:「这恐怕是第一本写给已经完全懂 C++ 的人看的书。」
与 《Effective C++》-第三版-55个改善程序与设计的具体做法-Scott-Meyers 相比:
- Effective C++ 是「条款式」的规则清单,体系化地告诉你「应该怎么做」
- Exceptional C++ 是「谜题式」的案例分析,通过解题让你自己发现「为什么应该这么做」
- 两本书互为补充,前者是地图,后者是迷宫探险
全书结构:11 章 47 条款
第 4 章:泛型编程与 C++ 标准库(条款 1-7)
核心问题:如何正确、高效地使用 STL 迭代器、算法、容器?
| 条款 | GotW # | 主题 | 难度 |
|---|---|---|---|
| 1 | #18 | Iterators 迭代器 | 7 |
| 2-3 | #29 | 不区分大小写的字符串 | - |
| 4-5 | #16 | 具有最大可复用性的通用容器 | - |
| 6 | #2 | 临时对象 | - |
| 7 | #3 | 使用标准库 | - |
关键要点:
- 迭代器失效是 STL 编程中最常见的陷阱之一:插入/删除元素后迭代器可能失效
- 永远优先使用标准库算法,而不是手写循环——更快、更安全、更少 bug
istream_iterator/ostream_iterator是输入输出与算法的桥梁- 不区分大小写的字符串比较:不要用
strcasecmp(C 风格、非标准),而应通过char_traits自定义或提供比较谓词 - 临时对象优化:理解 RVO(返回值优化) 和 拷贝消除,避免不必要的拷贝
第 5 章:异常安全的主题与相关技术(条款 8-19)
这是本书的核心章节,也是书名「Exceptional C++」的由来。
| 条款 | GotW # | 主题 | 难度 |
|---|---|---|---|
| 8-17 | #8 | 挑战篇——异常处理的安全性 | 9 |
| 18 | #20 | 代码的复杂性(一) | 9 |
| 19 | #21 | 代码的复杂性(二) | - |
异常安全的三个层次(从弱到强):
- 基本保证:操作失败后,对象仍处于一致状态(无资源泄漏、不变式成立)
- 强保证:操作要么完全成功,要么不产生任何效果(提交/回滚语义)
- 不抛出保证(nothrow):操作绝不会抛出异常
核心原则与技法:
- Copy-and-Swap 惯用法:先创建副本(可能抛异常),再用不抛异常的 swap 提交——实现强保证的经典手法
- Pimpl 惯用法:将实现细节隐藏在指针后,不仅降低编译依赖,也有助于异常安全
- 资源管理即对象管理:使用 RAII 类(
auto_ptr、智能指针、scoped_ptr等)自动管理资源 - 析构函数绝不抛出异常:否则会导致
std::terminate调用,程序直接终止
条款 18 经典案例:仅 4 行代码的函数
EvaluateSalaryAndReturnName,实际有 23 条执行路径——其中 3 条正常路径 + 20 条异常路径。这揭示了 C++ 异常带来的隐性复杂度。
第 6 章:类的设计与继承(条款 20-25)
| 条款 | GotW # | 主题 |
|---|---|---|
| 20 | #4 | Class 技术(构造/析构/赋值运算符) |
| 21 | #5 | 改写虚拟函数(Overriding Virtual Functions) |
| 22-23 | 14-15 | Classes 之间的关系 |
| 24 | - | 使用/滥用继承 |
| 25 | #13 | 面向对象程序设计 |
关键要点:
- 三大件:拷贝构造、拷贝赋值、析构——只要自己写了其中一个,通常三个都要写
- 虚函数重写规则:函数签名必须完全匹配(包括 const 性),否则不是重写而是隐藏
- is-a 原则:公有继承必须严格满足「派生类 IS-A 基类」的关系
- 继承是 C++ 中最强的耦合关系,慎用。优先考虑组合/委托
- LSP(Liskov 替换原则):派生类必须能在任何基类出现的地方替换之
第 7 章:编译级防火墙及 Pimpl Idiom(条款 26-30)
| 条款 | GotW # | 主题 |
|---|---|---|
| 26-28 | #7 | 编译期的依赖性 |
| 29 | #24 | 编译级防火墙 |
| 30 | #28 | ”Fast Pimpl” 技术 |
Pimpl(Pointer to Implementation)惯用法:
- 将类的私有成员封装到一个前置声明的实现类中,通过指针访问
- 核心收益:
- 降低编译依赖:修改实现不影响头文件,使用者无需重新编译
- 二进制兼容性:对象大小和布局不变
- 隐藏实现细节:头文件只暴露接口
- Fast Pimpl:使用自定义分配器(如固定大小内存池)避免每次 new/delete 的开销
- 与 《Effective STL》-50条有效使用STL的经验-Scott-Meyers 中关于智能指针的条款互相印证
第 8 章:名称搜索、命名空间、接口原则(条款 31-34)
| 条款 | GotW # | 主题 |
|---|---|---|
| 31 | #30 | 名称搜索(Name Lookup) |
| 32-34 | - | 接口原则(The Interface Principle) |
关键概念:
- ADL(Argument-Dependent Lookup,参数依赖查找):也叫 Koenig Lookup——调用函数时,如果参数的命名空间里有同名函数,也会被纳入候选集
- 接口原则:一个类的接口不仅包括它的成员函数,也包括与它在同一命名空间的非成员函数(因为 ADL 会找到它们)
- 这就是为什么
std::cout << x;不需要写std::operator<<——ADL 自动找到了std命名空间里的operator<<
第 9 章:内存管理(条款 35-37)
| 条款 | GotW # | 主题 |
|---|---|---|
| 35-36 | 9-10 | 内存管理(一)(二) |
| 37 | #25 | auto_ptr |
关键要点:
operator new/operator delete可以被类重载- Placement new:在已分配内存上构造对象
auto_ptr的转移所有权语义(C++98 时代的智能指针,C++11 后被unique_ptr取代)- 内存管理的黄金法则:谁分配谁释放,用 RAII 自动化
第 10 章:陷阱、易犯错误和反常作法(条款 38-41)
| 条款 | GotW # | 主题 |
|---|---|---|
| 38 | #11 | 对象等同问题(Object Identity) |
| 39 | #19 | 自动转换(Automatic Conversions) |
| 40-41 | 22-23 | 对象的生存期(Object Lifetimes) |
常见陷阱:
- 不要写
if( &x == &y )来判断对象等同——这比较的是地址,不是值 - 隐式类型转换是 bug 的温床:单参数构造函数加
explicit,转换运算符要谨慎 - 临时对象的生存期:绑定到 const 引用的临时对象生存期会延长到引用的作用域结束
- 悬垂指针/引用:永远不要返回局部对象的指针或引用
第 11 章:杂项主题(条款 42-47)
| 条款 | GotW # | 主题 |
|---|---|---|
| 42 | #1 | 变量的初始化 |
| 43 | #6 | 正确使用 const(const-correctness) |
| 44 | #17 | 类型转换(Casts) |
| 45 | #26 | bool |
| 46 | #27 | 转呼叫函数(Forwarding Functions) |
| 47 | #12 | 控制流(Control Flow) |
核心要点:
- const 正确性是 C++ 独有的强大特性:能用 const 就用 const——成员函数、参数、返回值
- 优先使用 C++ 风格的类型转换(
static_cast/dynamic_cast/const_cast/reinterpret_cast),不要用 C 风格强制转换 dynamic_cast是运行时类型识别,有性能开销,且往往暗示设计问题- 转发函数:用于完美转发参数到实际执行函数,减少代码重复
与其他 C++ 经典的关联
- 与 《Effective C++》-第三版-55个改善程序与设计的具体做法-Scott-Meyers 形成互补:Effective 告诉你规则,Exceptional 让你通过解题理解规则背后的原因
- 与 《More Exceptional C++》(如存在)是同系列续作
- 与 《Modern C++ Design》-现代C++设计-Andrei-Alexandrescu 同属进阶 C++ 设计范畴
- Pimpl 惯用法在大型项目中被广泛使用,是降低编译依赖的标准技术
可行动点
- 审视你的代码:找一个函数,数一下它有多少条异常路径——你可能会惊讶
- 检查异常安全等级:你的类提供的是基本保证、强保证还是 nothrow 保证?
- 运用 Pimpl:对于稳定度高的模块,考虑用 Pimpl 降低编译依赖
- const 正确性审查:给能加 const 的成员函数和参数都加上 const
- 优先使用标准库算法:替换手写循环为
std::for_each/std::find/std::transform等 - 警惕隐式转换:单参数构造函数加
explicit,减少意外的类型转换
版本说明
本笔记基于 7z 压缩包中的「Exceptional C++中文版(draft).pdf」——这是一个民间合译的草稿版本(约 80 页),非官方完整版。官方中文版由侯捷翻译,内容更完整、翻译质量更高。
英文版原书共约 300 页,包含更完整的代码示例和深入讨论。