摘要:很多资深C++开发者都有一个共同的疑问:为什么除了单例,我们很少在日常编码中“刻意”实现设计模式?本文将探讨C++语言特性如何用更简洁的方式取代传统设计模式的复杂结构,揭示设计模式是如何“隐形”存在标准库(STL)和主流框架。同时,分析在解决高复杂度、高变化性问题时,哪些模式是C++架构师不可或缺的。


一、C++设计模式的“隐形”之谜

有没有想过:在日常的C++代码除了随处可见的单例模式(Singleton),其他模式的“标准”实现鲜少露面。

1.1、为什么只看到单例?

设计模式,尤其是GoF(Gang of Four)提出的23种经典模式,提供可复用的、解决常见设计问题的方案。但 C++ 对设计模式的使用频率,有点不一样:

  1. 很多人学习设计模式时,看到的大部分都是基于Java或C#语言的、要大量接口和抽象类的实现示例。如果要把这些设计模式照搬到C++项目,会发现代码变得非常非常的冗长和复杂,降低可读性和效率。
  2. 而单例模式因为概念简单、实现直接,在全局配置、日志系统等场景中有非常明确的需求,成为C++代码库最容易见到和使用的模式。有一种错觉:如果一个模式不是像单例那样显眼,好像就不存在。

这是因为我们把设计模式看做一种固定的结构,而不是一种设计思想

1.2、C++语言特性对模式的“降维打击”

设计模式的诞生,很大程度上是为弥补早期面向对象语言在表达能力上的不足,或者解决某些语言在内存管理和行为抽象上的限制。但是,C++,特别是现代C++(C++11及以后版本),有一些独特的、强大的语言特性,这些特性能够用更简洁、更安全的方式实现某些模式的核心目标,让传统的模式实现变得多余。

代理模式为例。为安全管理资源或控制访问,要手动创建一个代理类来包裹实际的对象。但在C++:

  • 智能指针std::unique_ptrstd::shared_ptr本身就是资源管理代理。通过RAII机制,在编译期和运行时提供内存管理的代理,根本不用手动编写复杂的代理类结构。
  • 资源获取即初始化(RAII):这一C++特有的机制,通过对象的生命周期管理资源,从根本上就解决了很多要门面模式状态模式来处理的资源释放和状态切换问题。

C++ 的模板、泛型编程、Lambda表达式和std::function等特性,都提高了语言的抽象能力,让很多行为模式不再依赖传统的、基于虚函数的多态。

  • 策略模式的简化: 策略模式一般都要定义一个抽象基类和多个继承类。现代C++可以直接用std::function来存储和切换不同的算法(即策略),而算法本身可以是Lambda表达式、函数指针或仿函数。这种方式避免了创建大量的类定义文件,代码更加内联和高效。
  • 模板元编程: 模板和编译期多态可以实现比运行时多态更高效的结构扩展。能以更优雅的方式实现装饰器模式桥接模式 的功能,把设计模式的实现推向编译期,在运行时“隐形”。

所以,“C++中设计模式用得少”真正的含义是:很少要用GoF模式的重量级实现形式,因为C++语言本身已经提供更轻量、更高效的替代方案来实现模式背后的设计意图。 这就是C++设计模式的“隐形”。

二、语言特性如何“吞噬”经典模式

设计模式的价值是提供通用的、可复用的结构来解决软件设计中的常见问题。不过,一门语言本身提供有强大的机制来解决这些问题时,传统模式的复杂结构自然就会被“吞噬”或“简化”。现代C++(C++11/14/17/20/23)的特性就是这样,把模式的意图融入语言的惯用法中。

Classic GoF Design Patterns

Modern C++ Features & Idioms

Replaces/Simplifies Callables

Replaces/Simplifies Algorithms

Simplifies Hook Methods

Guarantees Thread-Safe Creation

Enforces Automatic Cleanup

Implements RAII for Memory

Built-in Language Feature

Simplifies Implementation of Wrappers

Alternative to Complex Type Hierarchies

Alternative to Abstract Factory (in specific cases)

std::function & Lambdas

Smart Pointers: std::unique_ptr, std::shared_ptr

C++11/14 Static Initialization Guarantee

Range-based for loops & STL Iterators

Variadic Templates & Fold Expressions

std::variant, std::optional, std::any

RAII (Resource Acquisition Is Initialization)

Command Pattern

Strategy Pattern

Template Method Pattern

Singleton Pattern

Resource Management (Manual Cleanup)

Iterator Pattern

Decorator / Proxy Pattern

Visitor Pattern (Partial Absorption)

Abstract Factory

2.1、RAII机制和资源管理

RAII (Resource Acquisition Is Initialization) 是C++的基石,用对象的生命周期来管理资源。对象创建时获取资源,对象销毁(无论是正常退出还是异常抛出)时通过析构函数自动释放资源。

代理模式或门面模式主要用来封装复杂的资源操作或生命周期管理。但C++ 的RAII机制通过语言层面就保证了资源的安全性,简化这方面的需求。

  • 智能指针 是一个完美的资源代理。封装了原始指针,并在析构时自动调用delete。这实现了代理模式中“控制访问”和“资源管理”的目的,不用自己手动编写代理类。
  • 锁卫兵(std::lock_guardstd::lock_guard在构造时锁定互斥量(Mutex),在析构时自动解锁。解决并发场景下资源访问的复杂性,用一种简洁、异常安全的方式实现门面模式(封装复杂的锁定/解锁流程)和代理模式(控制对共享资源的访问)。

RAII机制把资源管理的安全性和自动化融入语言本身,所以让那些为资源管理而生的重量级代理或门面模式在C++中变得没有必要。

2.2、Lambda和std::function对行为的抽象

面向对象设计要实现行为的动态切换或传递,必须依赖虚函数和继承,对应的就是策略模式命令模式。现代C++的函数对象、Lambda表达式和std::function在很大程度上都简化了这一过程。

策略模式的核心是定义一系列算法,并把它们封装起来,让它们可以相互替换。

  • 传统实现: 要定义一个IStrategy接口,然后为每种算法定义一个继承自IStrategy的具体类。
  • 现代C++实现: 可以用std::function<ReturnType(Args...)>来持有任何可调用对象。

示例:

// 传统模式要:
// class Comparator { public: virtual bool compare(int a, int b) = 0; };
// class AscendingComparator : public Comparator { ... };

// 现代C++实现:
#include <functional>
#include <algorithm>
#include <vector>

void sort_data(std::vector<int>& data, std::function<bool(int, int)> strategy) {
    std::sort(data.begin(), data.end(), strategy);
}

// 使用:
// 升序策略 (Lambda表达式)
sort_data(my_vec, [](int a, int b){ return a < b; }); 
// 降序策略 (std::greater)
sort_data(my_vec, std::greater<int>()); 

现代C++通过std::function和Lambda实现了策略模式的目标——运行时行为切换——已经不再用创建任何额外的类或复杂的继承结构,大大减少模板代码。

命令模式是把一个操作请求封装成一个对象,方便延迟执行、排队或支持撤销。

  • 传统实现: 要定义一个ICommand接口和多个具体命令类。
  • 现代C++实现: std::function<void()>可以直接封装一个无参数的命令。如果要支持撤销,可以定义一个包含两个std::functionexecuteundo)的简单结构体。

2.3、模板和泛型编程

C++的模板机制提供强大的编译期多态能力,有时候比运行时多态(虚函数)更灵活、性能更高,可以取代某些结构型模式。

桥接模式和装饰器模式主要用来把抽象和实现分离,或动态向对象添加职责。

  • 模板作为桥接: C++用模板参数来“桥接”不同的实现细节。比如,一个容器类可以通过模板参数来决定其内存分配策略,而不需要用运行时虚函数调用。
  • CRTP: CRTP可以让派生类继承自以自身为模板参数的基类。让基类可以在编译期访问派生类的成员,从而实现静态多态或功能注入。

功能注入(Mixin)和装饰: 通过模板和CRTP可以实现静态装饰器Mixin,在编译期把额外的功能注入到类中,避免装饰器模式在运行时引入的额外对象层级和虚函数开销。

现代C++的语言特性,特别是RAII、函数对象和模板,提供实现设计模式的更直接、更高效的“快捷方式”。不再需要依赖GoF模式的经典结构,而是把模式的思想融入日常的惯用法和标准库中,实现模式的“隐形化”。

三、框架和标准库中“内置”的模式

设计模式不是只存在亲手编写的业务逻辑代码中,而是存在 C++ 的生态系统。我们不是在“实现”模式,而是在“使用”模式,因为已经封装在标准模板库(STL)和各种主流框架的底层架构。

3.1、STL:迭代器模式的教科书级应用

标准模板库(STL)是C++泛型编程的典范,把容器、算法和迭代器三个概念解耦,而这种解耦是通过设计模式实现的。

迭代器模式提供一种方法顺序访问一个聚合对象的各个元素,又不暴露该对象的内部表示。

  • STL中的体现: std::vector::iterator, std::map::iterator, std::list::iterator等。
  • 价值: 无论底层数据结构是连续内存,还是基于节点,都可以用统一的接口(++*!=)来遍历它们。完美实现迭代器模式的目标:把遍历算法容器结构分离。
  • 现代用法: C++11引入的基于范围的for循环(for (auto& item : container))更是将迭代器模式的使用简化到了极致,让开发者几乎感觉不到迭代器的存在,但其底层机制依然是迭代器模式。

适配器模式把一个类的接口转换成客户希望的另一个接口。

  • STL中的体现: 容器适配器,如std::stack, std::queue, 和std::priority_queue
  • 价值: 它们不是新的容器,而是通过组合现有的容器(默认是std::dequestd::vector),并限制其接口,从而提供特定的行为。
  • std::stack适配了底层容器,只暴露push, pop, top接口,实现后进先出(LIFO)的行为。
  • std::queue适配了底层容器,只暴露push, pop, front接口,实现先进先出(FIFO)的行为。

3.2、GUI框架

C++ 最流行的GUI框架之一 Qt 就内置了对核心模式的优雅实现。

观察者模式定义对象间的一种一对多依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知并被自动更新。

  • Qt中的体现: Signals & Slots 机制。
  • 价值: Qt的Signals & Slots是C++生态最著名的观察者模式实现。Signal(信号) 相当于观察者模式的“主题”。Slot(槽) 相当于观察者模式的“观察者”。通过connect()函数建立连接,实现类型安全的、松耦合的通信。当一个对象发出信号时,所有连接到该信号的槽函数都会被自动调用,而信号的发出者不用知道或依赖任何具体的接收者类。

中介者模式用一个中介对象来封装一系列的对象交互。中介者模式让各对象不用显式相互引用,从而使其耦合松散。

  • Qt中的体现: 对话框或主窗口设计的一个父组件就是一个中介者。负责接收子组件发出的信号,协调其他子组件的响应。
  • 价值: 避免组件之间形成复杂的网状依赖关系,提高大型GUI应用的模块化和可维护。

3.3、编译器和解释器中的模式

即使是底层系统和工具链,模式也无处不在:访问者模式。

访问者模式是表示一个作用某对象结构的各元素的操作。可以在不改变各元素的类的前提下定义作用这些元素的新操作。

  • 应用场景: 编译器和解释器的抽象语法树(AST)遍历。
  • 价值: 编译器要对AST执行多种操作时,用访问者模式可以避免在核心的AST节点类不断添加新的方法,从而遵循开闭原则(对扩展开放,对修改关闭)。

所以,C++ 不是不用设计模式,而是因为这些模式已经被抽象、封装,成为日常使用的工具和框架的一部分。站在巨人的肩膀上,享受设计模式带来的架构优势,不用去重复实现其底层细节。

四、不可或缺的五大设计模式

现代C++的语言特性可以替代一些轻量级的模式,但在大型、高复杂度、高变化性的系统架构还是要依赖经典的GoF设计模式来提供结构化的解决方案。

在这里插入图片描述

4.1、抽象工厂 / 工厂方法

解决问题: 封装对象的创建过程,让系统在不指定具体类的情况下,能创建对象。实现“面向接口编程”和“依赖倒置原则”。

C++的工厂模式最常用来处理平台差异性可插拔性

  1. 跨平台组件创建: 软件要在Windows和Linux上运行,并且要创建平台相关的窗口对象和文件系统对象。抽象工厂可以定义一个GUIFactory接口,包含createWindow()createFS()方法。在运行时,系统根据操作系统加载WindowsFactoryLinuxFactory的具体实现,保证业务逻辑代码只依赖抽象接口,跟具体平台实现解耦。
  2. 插件系统/模块加载: 要动态加载DLL或SO文件,要求这些外部模块提供一个标准的接口来创建对象,工厂方法是唯一的选择。程序只知道工厂接口,不知道具体插件的实现细节。

4.2、观察者模式

解决问题: 严格解耦发布者和订阅者,实现一对多的依赖关系,保证状态变化能通知所有相关方。

Qt的Signals & Slots是观察者模式的优秀实现,但在不依赖Qt的纯C++环境,还是要手动实现或用Boost的Signal库。

  1. 数据模型-视图同步: 任何要把数据跟多个展示界面分离的架构,观察者模式是必不可少的。数据发生变化时,Model作为主题,通知所有注册的View进行自我更新,而Model本身不用知道有多少View,以及这些View是怎么渲染数据的。
  2. 日志和监控系统: 核心业务模块产生日志事件,不同的日志处理器订阅这些事件,实现灵活的日志分发。

4.3、命令模式

解决问题: 把一个操作请求封装为一个对象,从而可以对请求进行参数化、排队、记录日志,并支持**可撤销(Undo/Redo)**操作。

命令模式在要历史记录和回滚功能的复杂应用中是不可替代的。

  1. 图形编辑器或文本编辑器: 用户的每一个操作都被封装为一个Command对象。execute()方法执行操作。unexecute()方法执行逆操作。通过维护一个命令栈,实现无限级的撤销和重做功能。
  2. 事务管理: 把一系列数据库操作封装成一个命令对象,如果执行过程中出现错误,可以调用unexecute()或特定的回滚逻辑,保证数据的一致性。

4.4、适配器模式

解决问题: 解决接口不兼容性,让原本不能一起工作的类能协同工作。

适配器模式是处理外部依赖和历史遗留问题的“胶水代码”。

  1. 集成遗留 C API: C++经常要调用大量的C语言库。这些C库用结构体和函数指针。一个对象适配器可以封装这些C函数,把其转换为有现代C++风格(类、成员函数、异常安全)的接口,供项目其他部分使用。
  2. 统一数据库驱动: 应用要支持MySQL、PostgreSQL和SQLite。虽然它们功能相似,但API不同。可以定义一个统一的DBConnection接口,然后为每个数据库实现一个适配器类,把统一的接口调用转换为特定数据库驱动的函数调用。

4.5、装饰器模式

解决问题: 动态的给一个对象添加额外的职责。相比用继承来扩展功能,装饰器提供更大的灵活性。

装饰器模式在要灵活组合功能,且功能组合方式不确定的场景非常好用。

  1. I/O 流处理: 一个基础的文件写入器(FileWriter)。要动态添加功能:
    • CompressionDecorator:写入前压缩数据。
    • EncryptionDecorator:压缩后加密数据。
    • BufferedDecorator:添加缓冲机制提高性能。
    • 装饰器可以随意组合这些功能,不用为每种组合创建新的子类。
  2. 网络协议栈: 处理网络数据包时,每个装饰器可以代表协议栈中的一层,动态为原始数据包添加或剥离功能。

这五种模式(工厂、观察者、命令、适配器、装饰器)是C++架构师设计复杂、可扩展、低耦合系统必须掌握和显式应用的工具。超越语言特性所能提供的简单替代方案,解决宏观架构层面的组织和交互问题。

五、结语

设计模式的价值不在形式上的类图和继承结构,而在于背后的设计思想。

C++设计模式的使用:意图重于形式。追求的是模式带来的解耦、可扩展和可维护性,而不是严格遵循GoF书籍的类图。

模式意图 传统形式 (GoF) C++ 现代惯用法
资源管理 代理模式 RAII机制, 智能指针 (std::unique_ptr)
行为切换 策略模式 std::function, Lambda表达式
遍历抽象 迭代器模式 STL迭代器, Range-based for loop
功能注入 装饰器模式 模板, CRTP, Mixin

随着C++20 Modules、Concepts等新特性的普及,C++的抽象能力会进一步增强。

  • Concepts(概念) 泛型编程更加安全和易用,简化模板元编程复杂的模式实现,让基于模板的静态模式更加主流。
  • 模式的自动化: 很多现代工具和框架正在把设计模式的实现进一步自动化。

设计模式在C++中的地位不会削弱,只会变得更加成熟和内敛。从显眼的类层次结构,逐渐融入到语言的惯用法、标准库的接口以及框架的底层架构。

在这里插入图片描述

更多推荐