GoF 23 种设计模式在教科书上往往是一页页继承树。现代 C++ 里很多模式可以大幅瘦身std::function 替代策略接口、make_unique 管工厂、variant + visit 做访问者——少写类,多写意图。

这一篇对应 demo:ref/cpp_demo/basics/design_patterns/(Strategy、Observer、Builder、Singleton、Command、Visitor)。

这是「现代 C++ 实战」系列的第 15 篇。建议先读 第 14 篇:线程池与背压控制

一、模式还要学吗?

传统痛点 现代 C++ 的解
每种策略一个子类 std::function + lambda
Observer 抽象接口 + 虚函数 回调列表;对象观察者用 weak_ptr
复杂构造参数爆炸 Builder 链式 + 移动语义
new/delete 散落 make_unique 工厂 + RAII
为每种类型写 Visitor 子类 std::variant + std::visit(C++17)

模式解决的是反复出现的结构问题;语言特性升级后,实现方式变,意图不变

二、Strategy:算法可互换

意图:运行时切换算法,避免 if/elseswitch 堆叠。

传统:抽象 Strategy 接口 + AddStrategy / MultiplyStrategy

现代:

1
2
3
4
5
6
7
8
9
10
11
12
class Calculator {
public:
using Strategy = std::function<int(int, int)>;
explicit Calculator(Strategy s) : strategy_(std::move(s)) {}
void setStrategy(Strategy s) { strategy_ = std::move(s); }
int calculate(int a, int b) { return strategy_(a, b); }
private:
Strategy strategy_;
};

Calculator calc([](int a, int b) { return a + b; });
calc.setStrategy([](int a, int b) { return a * b; });
何时仍用继承 何时用 std::function
策略有复杂状态、多方法 单一可调用对象就够
需要编译期多态(Concepts) 运行时切换、配置驱动

三、Observer:发布-订阅

意图:主题状态变化时通知多个订阅者,彼此松耦合。

简单场景(demo EventPublisher):

1
2
3
4
5
6
7
8
9
10
class EventPublisher {
public:
using Observer = std::function<void(const std::string&)>;
void subscribe(Observer obs) { observers_.push_back(std::move(obs)); }
void notify(const std::string& event) {
for (const auto& obs : observers_) obs(event);
}
private:
std::vector<Observer> observers_;
};

对象型观察者时,用 weak_ptr 避免循环引用(Subject 持 Observer,Observer 又持 Subject):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Subject {
std::vector<std::weak_ptr<Observer>> subs_;
public:
void subscribe(std::weak_ptr<Observer> w) { subs_.push_back(w); }
void notify() {
for (auto it = subs_.begin(); it != subs_.end(); ) {
if (auto sp = it->lock()) {
sp->onUpdate();
++it;
} else {
it = subs_.erase(it); // 已销毁,清理
}
}
}
};

demo 用 lambda 注册三个「观察者」,模拟 MVC 里模型通知多个视图。

四、Builder:链式构建复杂对象

意图:分步设置可选参数,最后一次性构造不可变或半不可变对象。

demo 以 HttpRequest 为例:

1
2
3
4
5
6
HttpRequest req = HttpRequest::builder()
.setMethod("POST")
.setUrl("/api/users")
.addHeader("Content-Type", "application/json")
.setBody(R"({"name": "Alice"})")
.build();

要点:

  • 每个 setXxx 返回 Builder&(或 *this),支持链式
  • build() 调用私有构造函数,保证对象合法
  • 大字段用 移动 进目标对象,避免多余拷贝

适合:HTTP 请求、SQL 连接串、游戏 Character 配置等参数多的类型。

五、Factory:make_unique + 注册表

意图:创建逻辑集中,调用方依赖抽象而非具体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Widget { public: virtual ~Widget() = default; virtual void draw() = 0; };
class Button : public Widget { void draw() override { /* … */ } };
class Label : public Widget { void draw() override { /* … */ } };

using FactoryFn = std::function<std::unique_ptr<Widget>()>;

class WidgetFactory {
std::map<std::string, FactoryFn> registry_;
public:
void registerType(std::string name, FactoryFn fn) {
registry_[std::move(name)] = std::move(fn);
}
std::unique_ptr<Widget> create(const std::string& name) const {
auto it = registry_.find(name);
if (it == registry_.end()) throw std::invalid_argument("unknown type");
return it->second();
}
};

// 注册
WidgetFactory factory;
factory.registerType("button", [] { return std::make_unique<Button>(); });
factory.registerType("label", [] { return std::make_unique<Label>(); });

auto w = factory.create("button");

永远 make_unique,所有权清晰;插件式扩展时注册表可从配置文件或静态初始化填充。

六、RAII 与 Scope Guard

意图:资源获取即初始化,作用域结束自动释放——C++ 的默认模式,不是 GoF 23 之一,但贯穿所有模式。

1
2
3
4
5
6
7
{
std::lock_guard lock(mtx);
// 临界区
} // 自动 unlock

auto file = std::unique_ptr<FILE, decltype(&fclose)>(
fopen("data.txt", "r"), &fclose);

Scope Guard(C++11 起常用写法):在作用域退出时执行任意清理:

1
auto guard = finally([] { log("leave scope"); });

C++23 有 std::scope_exit;之前可用 small helper 或第三方 scope_guard。与 第 04 篇 的智能指针、lock_guard 一脉相承。

七、demo 中的更多模式

模式 demo 要点
Singleton Meyer 单例:static ConfigManager instance;,C++11 保证静态局部初始化线程安全
Command std::function<void()> 队列,支持宏命令(组合多条命令)
Visitor std::variant<Circle, Rectangle, Triangle> + std::visit 算面积

Visitor 现代写法:

1
2
3
4
5
6
7
8
9
10
using Shape = std::variant<Circle, Rectangle, Triangle>;

double area(const Shape& s) {
return std::visit([](const auto& shape) -> double {
using T = std::decay_t<decltype(shape)>;
if constexpr (std::is_same_v<T, Circle>)
return 3.14159 * shape.radius * shape.radius;
// … Rectangle, Triangle
}, s);
}

新增操作 = 新 visit 函数,不必改 Shape 类层次(类型集合需在设计期固定)。

八、模板 vs 虚函数:怎么选?

编译期(模板 / Concepts) 运行时(虚函数 / std::function)
性能 可内联、零开销抽象 虚表或 type erasure 有开销
灵活性 类型须在编译期可知 配置、插件、脚本驱动
二进制 每实例化一份代码 单份 vtable
典型 容器算法、Ranges UI 回调、策略切换

原则:热路径、类型集固定 → 模板;需要运行时替换、跨 DLL 边界 → 虚函数或 std::function

九、demo 运行

1
2
cd ref/cpp_demo/basics/design_patterns
./build.sh --run

依次输出 Strategy、Observer、Builder、Singleton、Command、Visitor 六段演示。

十、小结

模式 现代 C++ 抓手
Strategy std::function、lambda
Observer 回调 vector;对象观察者 + weak_ptr
Builder 链式 Builder& + build()
Factory make_unique + 注册表
RAII 智能指针、lock_guard、scope guard
Visitor variant + visit

现代 C++ 实战系列第 15 篇完。下一篇 GoogleTest 单元测试——没有测试的 C++ 项目等于裸奔。

系列导航

篇号 标题 状态
14 线程池与背压控制
15 现代设计模式(本篇)
16 GoogleTest 单元测试 下一篇

完整大纲见工作区 docs/CPP_SERIES_OUTLINE.md