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/else 或 switch 堆叠。

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

现代(demo 的 Calculator):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Calculator {
public:
using Strategy = std::function<int(int, int)>;

Calculator(Strategy strategy) : strategy_(strategy) {}

void setStrategy(Strategy strategy) {
strategy_ = strategy;
}

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; }); // 运行时切换

demo 为了直观按值拷贝 strategy;生产代码可以写成 strategy_(std::move(strategy))、构造函数加 explicit,省一次 std::function 拷贝并避免隐式转换。

何时仍用继承 何时用 std::function
策略有复杂状态、多方法 单一可调用对象就够
需要编译期多态(Concepts) 运行时切换、配置驱动

三、Observer:发布-订阅

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

简单场景(demo EventPublisher):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class EventPublisher {
public:
using Observer = std::function<void(const std::string&)>;

void subscribe(Observer observer) {
observers_.push_back(observer);
}

void notify(const std::string& event) {
std::cout << "发布事件: " << event << "\n";
for (const auto& observer : observers_) {
observer(event);
}
}

size_t observerCount() const {
return observers_.size();
}

private:
std::vector<Observer> observers_;
};

对象型观察者时,用 weak_ptr 避免循环引用(Subject 持 Observer,Observer 又持 Subject)。demo 没有这一版,下面是示意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Observer {
public:
virtual ~Observer() = default;
virtual void onUpdate() = 0;
};

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 为例(Builder 是嵌套类 HttpRequest::Builder,HttpRequest::builder() 返回一个新的 Builder):

1
2
3
4
5
6
7
8
9
HttpRequest request = HttpRequest::builder()
.setMethod("POST")
.setUrl("/api/users")
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer token123")
.setBody(R"({"name": "Alice", "age": 30})")
.build();

request.print();

要点:

  • 每个 setXxx / addHeader 返回 Builder&(return *this;),支持链式
  • build() 调用 HttpRequest 的私有构造函数,外部只能经由 Builder 创建对象
  • demo 的 setter 都是 const std::string& 拷贝;字段大时可改成按值接收再 移动 进目标对象,避免多余拷贝

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

五、Factory:make_unique + 注册表

意图:创建逻辑集中,调用方依赖抽象而非具体类。demo 没有 Factory 的演示,下面是示意代码:

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 起常用写法):在作用域退出时执行任意清理。标准库没有现成的,自己写一个小 helper 即可(C++17):

1
2
3
4
5
6
7
8
9
10
11
template <class F>
struct Finally {
F f;
~Finally() { f(); }
};
template <class F> Finally(F) -> Finally<F>;

{
auto guard = Finally{[] { std::puts("leave scope"); }};
// …
} // 离开作用域时打印

注意 scope_exit 并没有进入 C++23 标准,它只在 Library Fundamentals TS v3 里以 std::experimental::scope_exit 出现;实际项目常用 GSL 的 gsl::finally 或上面这种自写 helper。与 第 04 篇 的智能指针、lock_guard 一脉相承。

七、demo 中的更多模式

模式 demo 要点
Singleton Meyer 单例:static ConfigManager instance;,C++11 保证静态局部初始化线程安全
Command CommandInvoker 用 std::vector<std::function<void()>> 存命令,lambda 捕获 Light / Fan 引用;一个 lambda 里调用多个操作即宏命令
Visitor std::variant<Circle, Rectangle, Triangle> + std::visit:calculateArea 算面积、printInfo 打印信息

Visitor 现代写法(demo 的 calculateArea):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using Shape = std::variant<
struct Circle,
struct Rectangle,
struct Triangle
>;

// Circle / Rectangle / Triangle 的定义略

double calculateArea(const Shape& shape) {
return std::visit([](const auto& s) -> double {
using T = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<T, Circle>) {
return 3.14159 * s.radius * s.radius;
} else if constexpr (std::is_same_v<T, Rectangle>) {
return s.width * s.height;
} else if constexpr (std::is_same_v<T, Triangle>) {
return 0.5 * s.base * s.height;
}
return 0.0;
}, shape);
}

新增操作 = 新 visit 函数(demo 的 printInfo 就是第二个操作),不必改 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

单文件 design_patterns.cpp、单一 target design_patterns(C++17),main() 依次调用 demo_strategy_pattern() … demo_visitor_pattern(),输出 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 单元测试 ✅