《Modern C++ Design》读书笔记

基本信息

  • 书名: Modern C++ Design: Generic Programming and Design Patterns Applied
  • 作者: Andrei Alexandrescu
  • 页数: 285页
  • 文件大小: 1.92MB
  • 分类: C++ / 设计模式 / 泛型编程

核心主题

这本书开创性地展示了如何将设计模式与泛型编程结合,创造出可复用的”泛型组件”(Generic Components)。书中的Loki库提供了工业级的C++泛型组件实现。

核心理念:泛型组件

泛型组件是可用于多种场景的设计模板,编译器会展开生成具体代码。它们:

  • 减少样板代码
  • 更好地表达设计意图
  • 支持最小化重编码的设计结构复用

第一部分:技术基础

第1章 Policy-Based Class Design(策略类设计)

核心问题:如何实现高度可定制的类,同时保持接口简单?

解决方案:策略模式 + 模板元编程

关键概念:

  • 策略类(Policy Class):封装特定行为或功能
  • 策略组合:通过模板参数组合多个策略
  • 默认策略:让类在没有显式策略时也能工作
  • 不完全实例化:某些策略可以”缺失”,提供可选功能

示例:

template <
    typename ThreadingModel = SingleThreaded,
    typename SmallObjAlloc = DefaultSmallObjAlloc
>
class MyClass : public ThreadingModel, 
                protected SmallObjAlloc {
    // 使用策略提供的功能
};

关键洞察:

  • 策略类之间应该相互独立(无依赖)
  • 每个策略只关注一个关注点(单一职责)
  • 策略可以定义可选功能的接口

第2章 Techniques(编译期技术)

本章介绍泛型编程的基础工具:

2.1 编译期断言

  • 用模板特化在编译时检查条件
  • 比 assert() 更早发现问题

2.2 部分模板特化

  • 为特定类型组合提供专门实现
  • 是实现类型选择的基础

2.3 局部类(Local Classes)

  • 定义在函数内部的类
  • 只能作为模板参数传递

2.4 整数常量到类型的映射

  • 用枚举+模板特化实现编译期查表
  • 用于类型索引和静态映射

2.5 Type-to-Type Mapping

  • 核心模式:type2type<SomeType>
  • 用于在编译期传递类型信息
  • 解决函数模板不能传递类型的问题

2.6 Type Selection

  • select2:二分选择类型
  • select3:三分选择类型
  • 配合is_convertible等 trait 使用

2.7 编译期类型检测

  • is_convertible:检测类型转换关系
  • inheritance_checker:检测继承关系
  • is_base_of:基类检测

2.8 NullType 和 EmptyType

  • NullType:空类型列表的结束标志
  • EmptyType:空值类型,占位用
  • Typelist的基本组成部分

2.9 Type Traits

  • is_void、is_pointer、is_integral 等
  • 查询类型的属性
  • 配合 SFINAE 实现重载解析

第3章 Typelists(类型列表)

核心问题:如何在编译期处理任意数量的类型?

解决方案:递归链表式的类型列表

typedef TypeList<int, TypeList<char, NullType> > MyList;

关键操作:

  • Length<Typelist>:计算类型数量
  • IndexType<Typelist, N>:获取第N个类型
  • Append<Typelist, Type>:追加类型
  • Find<Typelist, Type>:查找类型
  • Eraser<Typelist, Type>:删除类型
  • ReplaceType<Typelist, Old, New>:替换类型

应用:

  • 多方法分派
  • 策略组合
  • 类型安全的变长参数

第二部分:泛型组件

第4章 小对象分配器

问题:频繁创建/销毁小对象导致内存碎片和性能开销

解决方案:预分配固定大小块,按需分配小对象

  • FixedAllocator<N>:固定大小 N 的分配器
  • SmallObjAllocator:通用小对象分配器(支持多种大小)

关键设计:

  • 分配块(chunk)包含固定大小的插槽
  • 使用空闲列表管理已释放的内存
  • 线程安全版本使用锁或原子操作

第5章 泛型函数对象

问题:如何让任意函数/函数对象以统一接口调用?

解决方案:Functor 模板 + 转发机制

Functor 支持:

  • 普通函数指针
  • 成员函数指针
  • 函数对象(operator())
  • lambda 表达式(现代C++)
Functor<int(int, int)> f = someFunction;
int result = f(3, 4);

应用:

  • 命令模式泛型化
  • Undo/Redo 支持
  • 回调注册

第6章 单例实现

问题:经典单例的线程安全、延迟初始化、生命周期管理问题

解决方案:SingletonHolder 模式

template <typename T>
class SingletonHolder {
    static T* instance_;
public:
    static T& GetInstance() {
        if (!instance_) {
            instance_ = new T;
        }
        return *instance_;
    }
};

关键改进:

  • Phoenix Singleton:解决”静默死亡”问题(程序启动顺序问题)
  • Longevity Singleton:控制单例的生命周期
  • 线程安全版本

第7章 智能指针

核心类型:

  • SmartPtr<T>:引用计数智能指针
  • 支持所有权转移
  • 线程安全版本
  • 数组支持

关键设计:

  • 引用计数对象单独分配
  • 自定义删除器
  • 隐式转换为原始指针

第8章 对象工厂

问题:如何在不暴露构造细节的情况下创建对象?

解决方案:Factory 泛型组件

// 注册类型
Factory::Register<Base, Derived>("key");
 
// 创建对象
Base* obj = Factory::Create("key");

扩展:Clone 工厂支持基于原型的创建

第9章 抽象工厂

问题:如何创建相关对象族而不指定具体类?

解决方案:泛型 AbstractFactory 组件

结合 Prototype 和 Factory 模式

第10章 Visitor 模式

泛型 Visitor:支持任意数量的参数类型

Acyclic Visitor:

  • 消除循环依赖
  • 每个类实现不同的 Visit 接口
  • 使用动态_cast 查找支持的方法

第11章 多方法

问题:C++只支持单分发,如何实现多分发?

解决方案:Double Dispatcher 模式

  • StaticDispatcher:编译期静态分派
  • BasicDispatcher:运行时对数时间分派
  • BasicFastDispatcher:运行时常数时间分派

应用:图形变换、碰撞检测等


附录:多线程支持

Loki 提供最小的多线程抽象:

ThreadingModel 策略:

  • SingleThreaded:无同步
  • ObjectLevelLockable:对象级锁(每对象一个互斥)
  • ClassLevelLockable:类级锁(每类一个互斥)

关键接口:

class MyClass : public ObjectLevelLockable<MyClass> {
    Lock lock(*this);  // RAII 锁
    // ... 线程安全操作
};

原子操作:

  • AtomicAdd、AtomicSubtract 等
  • 针对平台优化的整数原子操作

关键概念总结

概念说明应用
Policy-Based Design通过模板参数定制行为核心设计哲学
Typelist编译期类型链表类型元编程基础
Functors统一函数调用接口命令模式、回调
SingletonHolder线程安全单例全局状态管理
SmartPtr引用计数指针资源管理
Factory对象创建封装解耦创建与使用
Double Dispatcher多方法分派泛型Visitor、多态
ThreadingModel多线程策略线程安全组件

实践要点

  1. 策略设计原则:

    • 每个策略独立关注一个方面
    • 策略之间不应有依赖
    • 提供合理的默认策略
  2. 编译期编程:

    • 能编译期完成的工作不要放运行时
    • 类型安全优于运行时检查
    • 使用SFINAE和trait进行重载解析
  3. Loki库使用:

    • 可作为独立组件使用
    • 也可作为学习泛型模式的参考
    • 注意:Loki是C++98时代的产物,现代C++有更好的替代品

与其他知识的关联


行动点

  • 研究Loki库的GitHub仓库(如果还存在)
  • 理解Policy-Based Design在现代C++中的应用
  • 思考如何将Typelist思想应用到当前项目
  • 对比现代C++(C++11/14/17/20)中的替代方案