Modern C++ Design — 现代C++设计
Andrei Alexandrescu 经典巨著。C++ 模板元编程的里程碑之作,开创了 Policy-Based Design(基于策略的设计) 范式,催生了 Loki 库,深刻影响了 Boost 及后续 C++ 标准库的发展。
“Modern C++ Design is an important book. It demonstrates ‘generic patterns’ or ‘pattern templates’ as a powerful new way of creating extensible designs in C++.” — Herb Sutter
核心贡献
本书将设计模式与泛型编程相结合,提出了**泛型组件(Generic Components)**的概念——可复用的设计模板,由编译器生成样板代码,实现从设计到代码的无缝过渡。
五大核心技术贡献:
- Policy-Based Class Design — 基于策略的类设计(全书方法论基石)
- Typelists — 类型列表(编译期类型计算的基础数据结构)
- Template Metaprogramming 技术集 — 编译期断言、类型萃取、类型转换检测等
- 七个泛型组件 — Singleton / SmartPtr / Functor / Factory / AbstractFactory / Visitor / Multimethods
- Loki 库 — 随书开源的 C++ 模板库,全部组件的工业级实现
全书结构
全书分为两大部分,共 11 章:
Part I: Techniques(技术基础)
| 章节 | 主题 | 核心内容 |
|---|---|---|
| Ch.1 | Policy-Based Class Design | 策略类设计方法论,正交性分解,宿主类 + 多策略组合 |
| Ch.2 | Techniques | 编译期断言、偏特化、Int2Type/Type2Type、TypeTraits、继承检测 |
| Ch.3 | Typelists | 类型列表的定义、操作(增删改查去重排序)、生成类层次结构 |
| Ch.4 | Small-Object Allocation | 小对象分配器,四层架构:Chunk → FixedAllocator → SmallObjAllocator → SmallObject |
Part II: Components(泛型组件)
| 章节 | 组件 | 设计模式对应 | 策略数量 |
|---|---|---|---|
| Ch.5 | Generalized Functors | Command 模式 | — |
| Ch.6 | SingletonHolder | Singleton 模式 | Creation + Lifetime + ThreadingModel(3个策略) |
| Ch.7 | SmartPtr | 智能指针 | Ownership + Conversion + Checking + Storage(4个策略) |
| Ch.8 | Object Factories | Factory 模式 | FactoryErrorPolicy(1个策略) |
| Ch.9 | Abstract Factory | Abstract Factory 模式 | FactoryUnit(1个策略) |
| Ch.10 | Visitor | Visitor 模式 | CatchAll(1个策略) |
| Ch.11 | Multimethods | 多重分派 | — |
第一章:Policy-Based Class Design(基于策略的类设计)
核心理念
设计就是选择。设计的难点不在于没有方案,而在于方案太多——这些方案还可以组合,产生”邪恶的组合爆炸”(the evil multiplicity of design)。
传统做法:
- “全能接口”(Do-It-All Interface):一个类支持所有用法 → 臃肿、难维护、难学习
- 多继承:混入各种功能类 → 组合爆炸、命名冲突、二义性
Alexandrescu 的方案:将类正交分解为多个独立的”策略”(Policies),通过模板参数组合。
什么是 Policy
Policy 是一个类模板,它定义了一个明确的接口(一组隐式约定的类型和函数),实现某一方面的功能。宿主类(Host Class)通过 template template 参数接收策略,将部分功能委托给策略类。
HostClass<Policy1, Policy2, Policy3>
↓ ↓ ↓
组合了三个正交的设计维度
关键特性
- 丰富行为传播(Enriched Policies):策略类通过公有继承被宿主类继承,策略提供的附加接口自动”渗透”到宿主类
- 不完全实例化实现可选功能:如果策略不提供某接口,只要宿主类不调用用到该接口的函数,代码仍能编译通过——“不用就不付费”
- 自定义结构,不止自定义行为:策略不仅能改变行为,还能改变宿主类的内部数据结构
- 策略间的兼容性与转换:每个策略可以定义自己能接受/转换的其他策略,通过转换构造函数/转换运算符实现
分解原则
将一个类拆分为策略时,遵循两条准则:
- 本地化、命名、隔离设计决策——那些存在权衡、或可以有其他合理实现的部分
- 寻找正交的策略——策略之间不需要交互,可以独立变化
这是全书最重要的方法论。后续所有组件(Singleton、SmartPtr、Functor 等)都是这一设计哲学的具体实践。
第二章:Techniques(模板元编程基础技术)
本章是全书的工具箱,共 10 项基础技术:
| 技术 | 作用 |
|---|---|
| Compile-time Assertions | 在模板代码中生成有意义的编译期错误信息(STATIC_CHECK 宏) |
| Partial Template Specialization | 偏特化——不是对某一固定参数特化,而是对符合某一模式的一组参数特化 |
| Local Classes | 局部类——在模板函数内部定义类,实现闭包式效果 |
| Int2Type | 将整型常量映射为类型,实现基于布尔值/数值的编译期分派 |
| Type2Type | 类型到类型的映射,用函数重载替代 C++ 不支持的函数模板偏特化 |
| Select | 根据编译期布尔条件选择两种类型之一 |
| Conversion/Inheritance Detection | 编译期检测两个类型是否可转换、是否为同一类型、是否有继承关系 |
| TypeInfo | 包装 std::type_info,提供值语义和比较能力 |
| NullType / EmptyType | 模板元编程中的占位类型(类似 nil 和 unit) |
| TypeTraits | 通用类型萃取:isPointer、isReference、PointeeType、ReferredType 等 |
这些技术在 C++11 后大多被纳入标准(std::is_same、std::is_base_of、std::enable_if、std::conditional 等),但在 2001 年是革命性的。
第三章:Typelists(类型列表)
为什么需要 Typelists
很多时候你需要对一组类型做同一件事——比如为类层次结构中的每个类型生成一个虚函数,或者为一组类型自动生成访问者接口。普通泛型编程处理不了”类型的集合”。
定义
template <class T, class U>
struct Typelist {
typedef T Head;
typedef U Tail;
};约定:Typelist 是”合适的”(proper)——Head 永远是普通类型,Tail 只能是另一个 Typelist 或 NullType。
12 个基本操作(全部编译期计算)
| 操作 | 说明 |
|---|---|
| Length | 计算类型列表长度 |
| TypeAt | 按索引访问类型 |
| IndexOf | 查找某类型的索引 |
| Append | 追加类型或另一个列表 |
| Erase | 删除第一个出现的某类型 |
| EraseAll | 删除所有某类型 |
| NoDuplicates | 去除重复类型 |
| Replace | 替换某类型 |
| DerivedToFront | 将派生类排到基类前面(偏序排序) |
| MostDerived | 找到最派生的类型 |
所有操作通过递归模板 + 偏特化模式匹配实现。因为编译期没有”可变变量”,所以 typelist 处理是纯函数式的。
自动生成类层次结构
Loki 提供两个强大的生成器:
- GenScatterHierarchy —— 生成散射式继承层次(每个类型模板实例都是基类)
- GenLinearHierarchy —— 生成线性继承链(更省空间)
用途:为 Visitor 模式自动生成 Accept 接口,为 Abstract Factory 自动生成 Create 接口等。
第四章:Small-Object Allocation(小对象分配器)
问题
C++ 运行时多态与动态分配绑定,但默认的 ::operator new 针对大对象优化,分配小对象时:
- 速度慢(涉及堆锁、空闲链表搜索)
- 每个对象有不可忽视的内存开销(块头信息)
四层架构
SmallObject (基类,用户继承使用)
↓
SmallObjAllocator (多 FixedAllocator 组合,大小分派)
↓
FixedAllocator (固定大小分配器,Chunk 向量)
↓
Chunk (单个内存块,等分为多个小区块)
关键设计
- Chunk 用位图/索引跟踪空闲区块
- 小对象走 FixedAllocator,大对象转发给
::operator new - 利用 C++ 的 size-aware operator delete 获得释放大小
- 可配置线程模型策略
纯标准 C++ 实现,可移植。速度不是最快的胜在简单、健壮、可移植。
第五章:Generalized Functors(泛化函数对象)
Command 模式的泛化
Functor 封装了”一次延迟的函数调用”——可以是普通函数、函数对象、成员函数指针。封装后拥有值语义,可以存储、传递、绑定、链式调用。
核心能力
- 统一封装:函数、仿函数、成员函数 → 统一的 Functor 类型
- 类型安全:编译期检查参数和返回值类型
- Binding(绑定):将部分参数预先绑定,生成参数更少的 Functor
- Chaining(链式):将多个 Functor 串成一个
- 类型转换:参数和返回值支持隐式转换
- Undo/Redo:用 Functor 实现撤销/重做
设计要点
- 延迟类型知识:FunctorHandler 模板尽量推迟具体类型的确定,让编译器推导
- 一等语义:用值语义而非指针操作,大大简化绑定和链式的实现
- 小对象优化:FunctorImpl 继承自 SmallObject,减少分配开销
第六章:Implementing Singletons(实现 Singleton)
Singleton 的真正难点
不是”如何保证唯一实例”(语言支持足够),而是生命周期管理——尤其是析构顺序。
死亡引用问题(Dead Reference Problem)
单例 A 析构后,单例 B 的析构函数还去访问 A → 未定义行为。C++ 静态对象析构顺序与构造顺序相反,但跨编译单元的构造顺序未定义。
四种生命周期策略
| 策略 | 原理 | 适用场景 |
|---|---|---|
| DefaultLifetime | 编译器控制,标准静态对象 | 无依赖的简单单例 |
| Phoenix Singleton | 销毁后再访问就”浴火重生”重新创建 | 允许死后重生的场景 |
| Singleton with Longevity | 用 SetLongevity 手动设置寿命值,值大的后析构 | 有明确依赖关系的单例组 |
| Leaky Singleton | 故意不释放,交给 OS 在进程结束时回收 | 不关心泄漏的程序 |
SingletonHolder — 策略化的单例模板
template <
class T,
template <class> class CreationPolicy = CreateUsingNew,
template <class> class LifetimePolicy = DefaultLifetime,
template <class> class ThreadingModel = SingleThreaded
>
class SingletonHolder;三个正交策略:
- CreationPolicy:CreateUsingNew / CreateStatic / CreateUsingMalloc / CreateByPrototype
- LifetimePolicy:DefaultLifetime / PhoenixSingleton / SingletonWithLongevity
- ThreadingModel:SingleThreaded / ObjectLevelLockable / ClassLevelLockable
多线程与 Double-Checked Locking
线程安全的单例用 DCLP(Double-Checked Locking Pattern)优化:只在第一次创建时加锁,之后直接返回。
注意:本书出版时 DCLP 在 C++ 中因内存模型问题存在争议,C++11 后可用
std::call_once和静态局部变量线程安全初始化。
第七章:Smart Pointers(智能指针)
SmartPtr 的策略分解
SmartPtr 将智能指针的功能正交分解为 4 个独立策略:
| 策略 | 职责 | 内置实现 |
|---|---|---|
| OwnershipPolicy | 所有权管理 | RefCounted / RefCountedMT / COM / DeepCopy / NoCopy |
| ConversionPolicy | 隐式转换到裸指针 | AllowConversion / DisallowConversion |
| CheckingPolicy | 空指针检查 | AssertCheck / RejectNull / NoCheck |
| StoragePolicy | 存储方式 | DefaultSPStorage / ArrayStorage / LockedStorage |
所有权策略的谱系
- RefCounted:引用计数,最通用
- RefCountedMT:线程安全的引用计数
- DeepCopy:深拷贝,复制时克隆指向对象
- NoCopy:禁止复制(类似 unique_ptr 的早期版本)
- COM:COM 对象的 AddRef/Release
其他细节
- operator& 重载(地址运算符的陷阱)
- 相等/不等比较、排序比较
- const 正确性(SmartPtr to const / const SmartPtr)
- 数组支持
- 多线程安全
本章展示了 policy-based design 的威力——4 个策略各有 3-5 种实现,理论上可以组合出数十种不同的智能指针,而代码量远小于逐个实现。
第八章:Object Factories(对象工厂)
核心问题
C++ 没有运行时类型信息的运输能力(不能在运行时”传递类型”来创建对象)。需要用类型标识符 + 创建器函数的映射来模拟。
Factory 模板
template <
class AbstractProduct,
class IdentifierType,
class ProductCreator = AbstractProduct* (*)(),
template <typename, class> class FactoryErrorPolicy = DefaultFactoryError
>
class Factory;关键设计
- 自注册机制:每个具体产品类在自己的实现文件中向 Factory 注册自己,工厂不需要知道所有类型——低耦合
- 类型标识符:通常用字符串(可序列化、可读),也可用 int 等可排序类型
- 错误策略:未找到类型时的行为(抛异常 / 返回空 / 断言等)可配置
Clone Factory
工厂模式的变体:不是从 0 创建对象,而是克隆一个原型对象(利用虚函数的多态复制能力)。
第九章:Abstract Factory(抽象工厂)
泛型化的 Abstract Factory
用 Typelists 定义产品族,自动生成抽象工厂接口。
typedef AbstractFactory<TYPELIST_3(Soldier, Monster, SuperMonster)>
AbstractEnemyFactory;内部用 GenScatterHierarchy 为每个产品类型生成一个纯虚 Create 函数。
ConcreteFactory 的策略
- OpNewFactoryUnit:调用
new创建对象 - PrototypeFactoryUnit:克隆原型创建对象
抽象工厂 + typelist 的组合,使得增减产品族中的类型只需要修改 typelist,不需要改动工厂代码。
第十章:Visitor(访问者模式)
Visitor 的本质
在不修改类层次结构的前提下,为其添加虚函数。对稳定的类层次 + 多变的操作场景非常有用。
Visitor 的痛点
- 循环依赖:Visitor 接口依赖所有具体元素类,元素类也依赖 Visitor 基类——类层次结构一变,所有 Visitor 都要重编译
- 侵入式:需要在元素类中添加 Accept 函数
Acyclic Visitor(无环访问者)
打破循环依赖:Visitor 基类是空的,每个具体元素只依赖对应的 Visitor 模板接口。代价是每次访问需要 dynamic_cast,速度稍慢。
书中建议:大多数情况下用 Acyclic Visitor,只有性能极其关键时才用 GoF 原始版本。
泛型 Visitor 组件
用 typelists + 模板自动生成 Visitor 的所有 Visit 函数接口,消除手写重复代码。使用者只需:
- 定义元素类型的 typelist
- 写具体访问者的 Visit 函数
重复的接口生成工作全部由库完成。
第十一章:Multimethods(多重分派)
问题
C++ 的虚函数只对 this 做单分派(single dispatch)。有时需要根据两个或多个对象的动态类型来选择函数——典型场景是碰撞检测、相交判断等。
五种实现方案,从慢到快
| 方案 | 时间复杂度 | 原理 |
|---|---|---|
| Brute Force | O(n) | 逐个 dynamic_cast 尝试 |
| Brute Force (auto) | O(n) | 用 typelist 自动生成 switch 链 |
| Logarithmic Dispatcher | O(log n) | 按类型排序 + 二分查找 |
| Constant-Time (matrix) | O(1) | 二维函数指针矩阵 + 类型索引 |
| FastDispatcher | O(1) | 自动维护类型索引的矩阵分派 |
对称与不对称
- 对称分派:交换两个参数顺序也能正确匹配
- 不对称分派:左参数和右参数的处理逻辑不同(如碰撞检测中,玩家撞敌人和敌人撞玩家可能不同)
实际建议
- 类型少 → brute force 最简单
- 类型多且性能重要 → constant-time matrix dispatcher
- 将 dispatcher 实现为策略,可以灵活切换
全书方法论总结
核心洞察
- 设计就是选择的组合——用正交分解 + 策略组合对抗组合爆炸
- 编译期能做的事不要放到运行时——模板元编程的威力
- 库应该是开放的——暴露接口规范,让用户写自己的策略实现
- 聪明的技术应该服务于简洁——复杂的模板实现应该包裹在简单易用的接口之后
对现代 C++ 的影响
本书的许多技术在 C++11/14/17/20 中被语言和标准库吸收:
| 书中概念 | 现代 C++ 对应 |
|---|---|
| Policy-Based Design | 标准库的 Allocator、Traits、执行策略 |
| Typelists | variadic templates + parameter packs |
| Int2Type / Select | std::integral_constant / std::conditional |
| TypeTraits | <type_traits> 整个头文件 |
| Compile-time assertions | static_assert |
| Conversion detection | std::is_convertible / std::is_base_of |
| SmartPtr (policy-based) | std::unique_ptr + custom deleter, std::shared_ptr |
| Functor + binding | std::function + std::bind / lambda |
| Singleton with Longevity | 标准仍无直接对应,需自行实现 |
可以说,Modern C++ Design 定义了”现代 C++“的一个重要面向——模板元编程与泛型设计模式的结合。C++11 后的很多标准特性都是对本书探索方向的语言级确认。
Loki 库
随书发布的开源 C++ 模板库,包含了书中所有组件的完整实现:
- Typelist.h — 类型列表及全部操作
- SmallObj.h — 小对象分配器
- Functor.h — 泛化函数对象
- Singleton.h — SingletonHolder
- SmartPtr.h — SmartPtr
- Factory.h — 工厂与克隆工厂
- AbstractFactory.h — 抽象工厂
- Visitor.h — 泛型访问者
- Multimethods.h — 多重分派
- TypeTraits.h — 类型萃取
- Threading.h — 线程模型策略
官网(书中):http://www.awl.com/cseng/titles/0-201-70431-5/ Loki 后续由社区维护,托管在 SourceForge / GitHub。
关联笔记
- 《A Tour of C++》-第二版-Bjarne-Stroustrup — C++ 之父的现代 C++ 全景导览
- 《SICP》-计算机程序的构造和解释-第二版 — 另一本改变编程范式的经典(Lisp 方向)
- 《代码整洁之道》-Clean Code-Robert-C-Martin — 代码层面的设计原则
- 《重构》-改善既有代码的设计 — 代码坏味道与重构手法
- 《实现模式》-Implementation Patterns-Kent Beck — Kent Beck 的实现模式集
- 《敏捷软件开发》-原则模式与实践-Robert-C-Martin — SOLID 原则与设计模式的 C++ 实践