Appearance
Modern C++ 设计模式与最佳实践
同一批经典模式,在 C++11/17/20 下写法已经换代:能用值语义就不手写指针,能在编译期解决就不留到运行期,能用
std::function/variant/concepts 就不必继承。本专题按「经典模式 → 现代写法」对比整理,每条都配可折叠示例、回链与八股问法。
一、速查:老写法 → 现代写法
| 场景 | 老写法 | 现代写法(C++11/17/20) | 详见 |
|---|---|---|---|
| 全局唯一实例 | 裸指针 + 手写锁 / 双检锁 | 函数内 static(C++11 起线程安全) | 创建型 |
| 创建对象 | new + 裸指针 | std::make_unique 返回 unique_ptr | 创建型 |
| 复杂构造 | 手写 Builder 类 | 链式 / 聚合 + C++20 指定初始化器 | 创建型 |
| 多态拷贝 | 到处手写 clone() | CRTP 提供 clone / 值语义拷贝 | 创建型 |
| 隐藏实现 | 头文件暴露全部依赖 | Pimpl(编译防火墙 + ABI) | 结构型 |
| 树形结构 | 裸指针 + 虚基类 | unique_ptr + variant | 结构型 |
| 统一接口 | 手写适配类 | std::function / span / string_view | 结构型 |
| 算法可替换 | 虚基类策略 | std::function / policy 模板参数 | 行为型 |
| 事件通知 | 手写回调列表 | std::function + weak_ptr 防悬垂 | 行为型 |
| 多类型分派 | 双分派 Visitor | variant + visit | 行为型 |
| 撤销 / 命令 | 手写命令基类 | std::function<void()> + 栈 | 行为型 |
| 资源清理 | goto cleanup | RAII / scope guard | 惯用法 |
| 拷贝赋值 | 手写自赋值判断 | copy-and-swap | 惯用法 |
| 禁止拷贝 | 私有未实现 | = delete | 惯用法 |
| 类型混用 | int 到处传 | 强类型包装 / enum class | 惯用法 |
| 编译期分支 | SFINAE 重载堆 | if constexpr / concepts | 惯用法 |
| 可能为空 | 魔数 -1 / 输出参数 | std::optional | 日常利器 |
| 多类型存储 | union / 继承层次 | std::variant | 日常利器 |
| 只读字符串参数 | const std::string& | std::string_view | 日常利器 |
| 比较运算符 | 手写 6 个 | operator<=>(C++20) | 日常利器 |
| 类设计原则 | 拍脑袋继承 | SOLID + 组合 + 编译期多态 | 设计准则 |
二、五大板块
| 页面 | 内容 |
|---|---|
creational.md | 单例、工厂、建造者、原型 —— 对象创建 |
structural.md | Pimpl、组合、适配器、装饰器、桥接、享元 —— 结构组装 |
behavioral.md | 策略、观察者、访问者、命令、状态、NVI、迭代器 —— 行为与交互 |
idioms.md | Rule of Zero、copy-and-swap、scope guard、强类型、CRTP、类型擦除…… 核心惯用法 |
daily-tools.md | C++17/20 日常利器(结构化绑定、optional、variant、string_view、concepts…) |
principles.md | SOLID 与设计准则如何在 C++ 落地 |
三、高频八股自测(含答案)
1. 线程安全单例怎么写?为什么函数内 static 就线程安全?
答案
C++11 起要求函数内 static 局部变量的初始化是线程安全的(编译器加锁/保证只初始化一次),且初始化后访问无额外开销。老式双检锁易漏 volatile/内存序。详见 creational.md、deadlock-singleton.md。
2. 策略模式有哪几种实现?怎么选?
答案
① 虚函数(运行期可换、有虚调用开销);② std::function(灵活、可存任意可调用、有间接调用开销);③ policy 模板参数(编译期确定、零开销,但类型不同)。需要运行期切换用 ①②,追求性能与内联用 ③。详见 behavioral.md。
3. 观察者模式用裸指针回调有什么坑?现代怎么写?
答案
被观察者销毁后回调成悬垂指针。现代做法:回调用 std::function,持有观察者用 weak_ptr,触发时 lock() 检查存活,或注册返回 RAII 句柄自动注销。详见 behavioral.md。
4. Visitor 用 std::variant + std::visit 相比继承式 Visitor 好在哪?
答案
不用为每个类型写基类/双分派样板,类型集合封闭时极简;配合 overloaded 重载集直接分派。缺点是新增类型要改所有 visit 点(封闭集合换来简洁)。详见 behavioral.md。
5. Pimpl 除了"减少编译依赖"还有什么好处?
答案
① 编译防火墙(头文件不含实现依赖);② 稳定 ABI(私有成员变化不影响二进制);③ 析构定义在 .cpp,配合 unique_ptr<Impl> 顺带获得强异常安全、便于 Rule of Zero。详见 structural.md、engineering.md。
6. copy-and-swap 解决了什么问题?
答案
一次 swap 同时解决异常安全(副本构造成功才交换,失败不影响原对象)和代码重复/自赋值(拷贝赋值与构造复用)。详见 idioms.md、atomic-failure.md。
7. 什么是 Rule of Zero?
答案
能用标准库成员(vector、string、智能指针)表达资源,就不写析构/拷贝/移动,让编译器默认生成——默认的最安全、最不易错。反之若自定义了其中一个,就要按 Rule of 3/5 补齐。详见 idioms.md。
8. std::optional / 指针 / 异常,错误处理怎么选?
答案
"可能没有值"且不是错误 → optional;"可能失败且需要原因" → 异常或 expected;需要表达所有权才用指针。别用魔数或输出参数表达"空"。详见 daily-tools.md、optional.md。
9. std::string_view 什么时候会悬垂?
答案
当它指向的字符串是临时对象或在 view 使用前已被销毁/修改(如 view = f() 返回的临时 std::string、或 string 扩容使缓冲失效)。view 非拥有,只适合"函数参数"这种生命周期受控场景。详见 daily-tools.md。
10. std::variant 与 union 的区别?
答案
variant 是类型安全的 tagged union:自动管理构造/析构、知道当前激活类型、可 visit;union 不追踪类型、非平凡类型需手管生命周期。详见 daily-tools.md。
11. concepts 相比 SFINAE 好在哪?
答案
可读性(具名约束)、诊断信息友好(直接说哪个约束不满足而非模板报错地狱)、可重载与组合、编译更快。SFINAE 仍用于需要剔除重载的场景。详见 idioms.md、concepts.md。
12. 强类型包装(newtype)解决什么问题?
答案
防止"同为 int 的参数传错位置"(userId 传给 orderId)、把单位/语义写进类型,让编译器在编译期抓住错误。详见 idioms.md、correctness-first.md。
13. 虚函数 / 模板 / std::function,多态怎么选?
答案
需要运行期异构替换且接口稳定 → 虚函数;类型编译期已知、追求零开销 → 模板/concepts;要存"任意可调用"、灵活但可接受间接调用 → std::function。优先编译期多态。详见 principles.md、zero-overhead.md。
14. 什么是 scope guard?
答案
用 RAII 在作用域退出时执行清理(gsl::finally / 手写 scope_exit),替代分散的 goto cleanup 或重复解锁代码,异常路径也能保证执行。详见 idioms.md、raii.md。
15. 指定初始化器(C++20)能替代 Builder 吗?
答案
对"简单聚合、字段可默认"的场景可以:Point p{.x=1, .y=2} 直观且免样板。但需要"分步校验/不可变构造/复杂依赖"时仍用 Builder 或工厂。详见 creational.md。
相关
- 设计思想(为什么这么设计):
design/ - 模板与元编程:
metaprogramming/ - 智能指针与所有权:
smart-pointers/ - C++11-20 新特性:
modern-cpp/ - 并发与线程安全单例:
concurrency/