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)**的概念——可复用的设计模板,由编译器生成样板代码,实现从设计到代码的无缝过渡。

五大核心技术贡献:

  1. Policy-Based Class Design — 基于策略的类设计(全书方法论基石)
  2. Typelists — 类型列表(编译期类型计算的基础数据结构)
  3. Template Metaprogramming 技术集 — 编译期断言、类型萃取、类型转换检测等
  4. 七个泛型组件 — Singleton / SmartPtr / Functor / Factory / AbstractFactory / Visitor / Multimethods
  5. Loki 库 — 随书开源的 C++ 模板库,全部组件的工业级实现

全书结构

全书分为两大部分,共 11 章:

Part I: Techniques(技术基础)

章节主题核心内容
Ch.1Policy-Based Class Design策略类设计方法论,正交性分解,宿主类 + 多策略组合
Ch.2Techniques编译期断言、偏特化、Int2Type/Type2Type、TypeTraits、继承检测
Ch.3Typelists类型列表的定义、操作(增删改查去重排序)、生成类层次结构
Ch.4Small-Object Allocation小对象分配器,四层架构:Chunk → FixedAllocator → SmallObjAllocator → SmallObject

Part II: Components(泛型组件)

章节组件设计模式对应策略数量
Ch.5Generalized FunctorsCommand 模式—
Ch.6SingletonHolderSingleton 模式Creation + Lifetime + ThreadingModel(3个策略)
Ch.7SmartPtr智能指针Ownership + Conversion + Checking + Storage(4个策略)
Ch.8Object FactoriesFactory 模式FactoryErrorPolicy(1个策略)
Ch.9Abstract FactoryAbstract Factory 模式FactoryUnit(1个策略)
Ch.10VisitorVisitor 模式CatchAll(1个策略)
Ch.11Multimethods多重分派—

第一章: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>
       ↓           ↓          ↓
   组合了三个正交的设计维度

关键特性

  1. 丰富行为传播(Enriched Policies):策略类通过公有继承被宿主类继承,策略提供的附加接口自动”渗透”到宿主类
  2. 不完全实例化实现可选功能:如果策略不提供某接口,只要宿主类不调用用到该接口的函数,代码仍能编译通过——“不用就不付费”
  3. 自定义结构,不止自定义行为:策略不仅能改变行为,还能改变宿主类的内部数据结构
  4. 策略间的兼容性与转换:每个策略可以定义自己能接受/转换的其他策略,通过转换构造函数/转换运算符实现

分解原则

将一个类拆分为策略时,遵循两条准则:

  1. 本地化、命名、隔离设计决策——那些存在权衡、或可以有其他合理实现的部分
  2. 寻找正交的策略——策略之间不需要交互,可以独立变化

这是全书最重要的方法论。后续所有组件(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 封装了”一次延迟的函数调用”——可以是普通函数、函数对象、成员函数指针。封装后拥有值语义,可以存储、传递、绑定、链式调用。

核心能力

  1. 统一封装:函数、仿函数、成员函数 → 统一的 Functor 类型
  2. 类型安全:编译期检查参数和返回值类型
  3. Binding(绑定):将部分参数预先绑定,生成参数更少的 Functor
  4. Chaining(链式):将多个 Functor 串成一个
  5. 类型转换:参数和返回值支持隐式转换
  6. 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 函数接口,消除手写重复代码。使用者只需:

  1. 定义元素类型的 typelist
  2. 写具体访问者的 Visit 函数

重复的接口生成工作全部由库完成。


第十一章:Multimethods(多重分派)

问题

C++ 的虚函数只对 this 做单分派(single dispatch)。有时需要根据两个或多个对象的动态类型来选择函数——典型场景是碰撞检测、相交判断等。

五种实现方案,从慢到快

方案时间复杂度原理
Brute ForceO(n)逐个 dynamic_cast 尝试
Brute Force (auto)O(n)用 typelist 自动生成 switch 链
Logarithmic DispatcherO(log n)按类型排序 + 二分查找
Constant-Time (matrix)O(1)二维函数指针矩阵 + 类型索引
FastDispatcherO(1)自动维护类型索引的矩阵分派

对称与不对称

  • 对称分派:交换两个参数顺序也能正确匹配
  • 不对称分派:左参数和右参数的处理逻辑不同(如碰撞检测中,玩家撞敌人和敌人撞玩家可能不同)

实际建议

  • 类型少 → brute force 最简单
  • 类型多且性能重要 → constant-time matrix dispatcher
  • 将 dispatcher 实现为策略,可以灵活切换

全书方法论总结

核心洞察

  1. 设计就是选择的组合——用正交分解 + 策略组合对抗组合爆炸
  2. 编译期能做的事不要放到运行时——模板元编程的威力
  3. 库应该是开放的——暴露接口规范,让用户写自己的策略实现
  4. 聪明的技术应该服务于简洁——复杂的模板实现应该包裹在简单易用的接口之后

对现代 C++ 的影响

本书的许多技术在 C++11/14/17/20 中被语言和标准库吸收:

书中概念现代 C++ 对应
Policy-Based Design标准库的 Allocator、Traits、执行策略
Typelistsvariadic templates + parameter packs
Int2Type / Selectstd::integral_constant / std::conditional
TypeTraits<type_traits> 整个头文件
Compile-time assertionsstatic_assert
Conversion detectionstd::is_convertible / std::is_base_of
SmartPtr (policy-based)std::unique_ptr + custom deleter, std::shared_ptr
Functor + bindingstd::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。


关联笔记