Skip to content

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 防悬垂行为型
多类型分派双分派 Visitorvariant + visit行为型
撤销 / 命令手写命令基类std::function<void()> + 栈行为型
资源清理goto cleanupRAII / 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.mdPimpl、组合、适配器、装饰器、桥接、享元 —— 结构组装
behavioral.md策略、观察者、访问者、命令、状态、NVI、迭代器 —— 行为与交互
idioms.mdRule of Zero、copy-and-swap、scope guard、强类型、CRTP、类型擦除…… 核心惯用法
daily-tools.mdC++17/20 日常利器(结构化绑定、optional、variant、string_view、concepts…)
principles.mdSOLID 与设计准则如何在 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。

相关 ​

糌粑的面经笔记 · VitePress 版