设计模式是面向对象编程的基石,但许多经典教材中的实现仍停留在C++98时代。随着C++17的std::optional和结构化绑定、C++20的Concepts与Ranges、C++23的std::expected和std::print,我们有更优雅、更安全、更高效的方式来重写这些模式。本文将从创建型、结构型、行为型三大类中选取最具代表性的模式,用现代C++逐一重构,并分析每种模式在现代C++下的新形态与取舍。
一、为什么需要用现代C++重写设计模式
GoF的《设计模式》出版于1994年,当时的C++还没有智能指针、没有lambda、没有变参模板、没有Concepts。许多模式的存在是为了弥补语言表达能力的不足。例如:
- Strategy模式在C++98中需要手动管理对象生命周期,而现代C++的lambda和std::function让策略传递变得极其自然
- Observer模式在C++98中需要手写订阅/通知逻辑,而C++23的std::expected和信号槽库让错误传播更优雅
- Factory模式在C++98中需要繁琐的类型注册,而C++20的Concepts让工厂的约束检查前置到编译期
- Iterator模式已被C++20 Ranges彻底重新定义,从手工迭代器走向组合式管道
这不是说设计模式过时了——它们背后的设计意图永远不会过时。但实现方式应该随语言进化而进化。下面我们逐一重构。
二、创建型模式:工厂、构建者与单例的现代重生
2.1 抽象工厂 + C++20 Concepts
经典抽象工厂的问题在于:没有任何编译期约束保证工厂能创建正确的类型。C++20的Concepts完美解决了这个问题:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 #include <concepts>
#include <memory>
#include <string>
// 用Concept定义产品接口约束
template<typename T>
concept Widget = requires(T t, const std::string& name) {
{ t.render() } -> std::same_as<void>;
{ t.name() } -> std::convertible_to<std::string>;
};
// 具体产品
class LinuxButton {
std::string name_;
public:
explicit LinuxButton(std::string name) : name_(std::move(name)) {}
void render() { std::println("[Linux] Button: {}", name_); }
std::string name() const { return name_; }
};
class WindowsButton {
std::string name_;
public:
explicit WindowsButton(std::string name) : name_(std::move(name)) {}
void render() { std::println("[Windows] Button: {}", name_); }
std::string name() const { return name_; }
};
// 工厂Concept——约束工厂必须能创建满足Widget的产品
template<typename F>
concept WidgetFactory = requires(F f, std::string name) {
{ f.create_button(name) } -> Widget;
};
// 具体工厂
class LinuxFactory {
public:
LinuxButton create_button(std::string name) {
return LinuxButton(std::move(name));
}
};
class WindowsFactory {
public:
WindowsButton create_button(std::string name) {
return WindowsButton(std::move(name));
}
};
// 客户端代码——Concept约束确保编译期类型安全
template<WidgetFactory F>
void build_ui(F& factory) {
auto btn = factory.create_button("Submit");
btn.render();
}
与C++98版本的关键区别:不再需要抽象基类
1 | IButton |
和
1 | IFactory |
,Concept在编译期完成了类型约束检查,零运行时开销。如果某个工厂的
1 | create_button |
返回的类型不满足
1 | Widget |
概念,编译器会立即报错并给出清晰的错误信息。
2.2 Builder模式 + 链式调用与std::expected
Builder模式在现代C++中的最大改进是:用
1 | std::expected |
(C++23)替代异常来处理构建校验错误:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 #include <expected>
#include <string>
#include <string_view>
struct ServerConfig {
std::string host;
int port;
int timeout_ms;
int max_connections;
bool use_tls;
// 校验函数,返回expected
std::expected<void, std::string> validate() const {
if (host.empty()) return std::unexpected("host cannot be empty");
if (port <= 0 || port > 65535) return std::unexpected("invalid port");
if (timeout_ms <= 0) return std::unexpected("timeout must be positive");
if (max_connections <= 0) return std::unexpected("max_connections must be positive");
return {};
}
};
class ServerConfigBuilder {
ServerConfig cfg_{"localhost", 8080, 5000, 100, false};
public:
ServerConfigBuilder& host(std::string_view h) { cfg_.host = h; return *this; }
ServerConfigBuilder& port(int p) { cfg_.port = p; return *this; }
ServerConfigBuilder& timeout(int ms) { cfg_.timeout_ms = ms; return *this; }
ServerConfigBuilder& max_conn(int n) { cfg_.max_connections = n; return *this; }
ServerConfigBuilder& enable_tls() { cfg_.use_tls = true; return *this; }
// 构建时校验,失败返回expected的unexpected
std::expected<ServerConfig, std::string> build() {
if (auto result = cfg_.validate(); !result) {
return std::unexpected(result.error());
}
return cfg_;
}
};
// 使用
auto config = ServerConfigBuilder{}
.host("api.example.com")
.port(443)
.enable_tls()
.build();
if (!config) {
std::println("Build failed: {}", config.error());
} else {
std::println("Config: {}:{} timeout={}ms max_conn={}",
config->host, config->port, config->timeout_ms, config->max_connections);
}
2.3 单例模式 + Meyers’ Singleton的线程安全演进
C++11起,局部静态变量的初始化就保证了线程安全。但现代C++对单例的需求已经发生了根本变化——依赖注入(DI)框架和可测试性要求让传统单例变得不合时宜。推荐的现代做法是:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 #include <mutex>
#include <memory>
// 方案1:Meyers' Singleton——最简形式,C++11起线程安全
class Logger {
Logger() = default;
public:
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
static Logger& instance() {
static Logger logger; // C++11保证线程安全初始化
return logger;
}
void log(std::string_view msg) {
std::println("[LOG] {}", msg);
}
};
// 方案2:可测试的依赖注入单例——允许在测试中替换
class ConfigProvider {
inline static std::shared_ptr<ConfigProvider> instance_;
inline static std::mutex mtx_;
protected:
ConfigProvider() = default;
public:
virtual ~ConfigProvider() = default;
virtual std::string get(std::string_view key) const = 0;
static std::shared_ptr<ConfigProvider> get_instance() {
std::lock_guard lock(mtx_);
return instance_;
}
// 仅在测试中使用
static void set_instance(std::shared_ptr<ConfigProvider> p) {
std::lock_guard lock(mtx_);
instance_ = std::move(p);
}
static void init() {
std::lock_guard lock(mtx_);
if (!instance_) {
instance_ = std::make_shared<DefaultConfigProvider>();
}
}
};
三、结构型模式:适配器、装饰器与代理的现代实现
3.1 适配器模式 + std::variant与std::visit
当需要适配多种不相关的第三方接口时,
1 | std::variant |
+
1 | std::visit |
比传统继承体系更简洁:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 #include <variant>
#include <vector>
#include <string>
// 三种不兼容的日志库接口
struct LibALogger {
void write_a(const char* msg) { /* LibA的实现 */ }
};
struct LibBLogger {
int log_b(std::string_view msg) { /* LibB的实现 */ return 0; }
};
struct LibCLogger {
void emit(int level, const std::string& msg) { /* LibC的实现 */ }
};
// 统一日志接口——用variant适配多种后端
using LoggerBackend = std::variant<LibALogger, LibBLogger, LibCLogger>;
class UnifiedLogger {
std::vector<LoggerBackend> backends_;
public:
void add_backend(LoggerBackend backend) {
backends_.push_back(std::move(backend));
}
void log(std::string_view msg) {
for (auto& backend : backends_) {
std::visit([&](auto& logger) {
using T = std::decay_t<decltype(logger)>;
if constexpr (std::is_same_v<T, LibALogger>) {
logger.write_a(msg.data());
} else if constexpr (std::is_same_v<T, LibBLogger>) {
logger.log_b(msg);
} else if constexpr (std::is_same_v<T, LibCLogger>) {
logger.emit(1, std::string(msg));
}
}, backend);
}
}
};
1 | std::variant |
的优势在于:所有类型在编译期已知,没有虚函数调用开销,且
1 | if constexpr |
分支在编译期展开,零运行时分支。这在性能敏感场景中是巨大的优势。
3.2 装饰器模式 + 通用lambda包装
传统装饰器需要为每个组件接口写一个装饰器类。现代C++可以用
1 | std::function |
和lambda组合来消除大量样板代码:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56 #include <functional>
#include <vector>
#include <string>
#include <chrono>
#include <map>
// 用std::function定义可装饰的处理器类型
using RequestHandler = std::function<std::string(std::string_view)>;
// 基础处理器
RequestHandler make_base_handler() {
return [](std::string_view req) -> std::string {
return "Handled: " + std::string(req);
};
}
// 装饰器:日志
RequestHandler with_logging(RequestHandler inner) {
return [inner = std::move(inner)](std::string_view req) -> std::string {
std::println("[LOG] Request: {}", req);
auto result = inner(req);
std::println("[LOG] Response: {}", result);
return result;
};
}
// 装饰器:计时
RequestHandler with_timing(RequestHandler inner) {
return [inner = std::move(inner)](std::string_view req) -> std::string {
auto start = std::chrono::steady_clock::now();
auto result = inner(req);
auto elapsed = std::chrono::steady_clock::now() - start;
auto ms = std::chrono::duration_cast<std::chrono::microseconds>(elapsed);
std::println("[TIMER] Took {}us", ms.count());
return result;
};
}
// 装饰器:缓存
RequestHandler with_cache(RequestHandler inner) {
return [inner = std::move(inner), cache = std::map<std::string, std::string>{}]
(std::string_view req) mutable -> std::string {
std::string key(req);
if (auto it = cache.find(key); it != cache.end()) {
std::println("[CACHE] Hit for: {}", req);
return it->second;
}
auto result = inner(req);
cache[key] = result;
return result;
};
}
// 链式组合——比继承装饰器优雅得多
auto handler = with_logging(with_timing(with_cache(make_base_handler())));
auto result = handler("GET /api/users");
四、行为型模式:策略、观察者与状态机的现代演进
4.1 策略模式 + Lambda与std::function
策略模式可能是从现代C++中获益最大的模式。在C++98中,你需要定义抽象策略类、具体策略类、上下文类。现在一个lambda就够了:
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
26
27
28
29
30
31
32
33
34
35 #include <functional>
#include <vector>
#include <algorithm>
#include <ranges>
class Sorter {
public:
using SortStrategy = std::function<void(std::vector<int>&)>;
private:
SortStrategy strategy_;
public:
explicit Sorter(SortStrategy strategy) : strategy_(std::move(strategy)) {}
void sort(std::vector<int>& data) { strategy_(data); }
void set_strategy(SortStrategy strategy) {
strategy_ = std::move(strategy);
}
};
// 使用——lambda即策略,无需定义任何类
std::vector<int> data = {5, 2, 8, 1, 9, 3};
Sorter sorter([](std::vector<int>& v) {
std::ranges::sort(v); // C++20 Ranges
});
sorter.sort(data);
// 运行时切换策略
sorter.set_strategy([](std::vector<int>& v) {
std::ranges::sort(v, std::greater<>{}); // 降序
});
sorter.sort(data);
4.2 观察者模式 + RAII自动断开
传统观察者模式需要手写订阅/取消/通知逻辑,且存在生命周期管理难题(悬挂指针)。现代C++用RAII管理订阅,用
1 | std::function |
替代虚函数:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 #include <functional>
#include <vector>
#include <memory>
#include <list>
#include <cstddef>
template<typename... Args>
class Signal {
using Slot = std::function<void(Args...)>;
struct SlotData {
size_t id;
Slot slot;
};
std::list<SlotData> slots_; // list保证迭代器稳定性
size_t next_id_ = 0;
public:
// 连接槽,返回RAII断开器
[[nodiscard]] auto connect(Slot slot) {
size_t id = next_id_++;
auto it = slots_.insert(slots_.end(), {id, std::move(slot)});
// 返回RAII guard——析构时自动断开
return std::unique_ptr<void, std::function<void(void*)>>(
reinterpret_cast<void*>(id),
[this, it](void*) {
slots_.erase(it);
}
);
}
void emit(Args... args) {
for (auto& [id, slot] : slots_) {
slot(args...);
}
}
};
// 使用示例
struct EventManager {
Signal<std::string> on_message;
Signal<int, int> on_resize;
};
// RAII管理订阅生命周期
EventManager mgr;
auto conn1 = mgr.on_message.connect([](std::string msg) {
std::println("Handler1: {}", msg);
});
auto conn2 = mgr.on_message.connect([](std::string msg) {
std::println("Handler2: {}", msg);
});
mgr.on_message.emit("Hello");
conn1.reset(); // 自动断开
mgr.on_message.emit("World"); // 只有Handler2响应
这个实现的亮点:RAII自动断开解决了传统观察者模式最大的痛点——悬挂指针。当
1 | conn1 |
被
1 | reset() |
或离开作用域时,槽自动从信号中断开,不会触发已销毁对象的回调。
4.3 状态模式 + std::variant与编译期状态机
状态模式在C++98中需要为每个状态定义子类。C++17的
1 | std::variant |
让我们可以把所有状态编码在类型系统中,用
1 | std::visit |
做状态转移:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72 #include <variant>
#include <string>
#include <println>
// 状态定义——每个状态是一个独立类型,可携带数据
struct Idle {
std::string name = "Idle";
};
struct Connecting {
std::string host;
int port;
int retry_count = 0;
};
struct Connected {
int connection_id;
std::string peer_info;
};
struct Error {
std::string message;
int error_code;
};
using ConnectionState = std::variant<Idle, Connecting, Connected, Error>;
// 状态转移函数
ConnectionState handle_event(ConnectionState current, std::string_view event) {
return std::visit([&](auto& state) -> ConnectionState {
using T = std::decay_t<decltype(state)>;
if constexpr (std::is_same_v<T, Idle>) {
if (event == "connect") {
return Connecting{"api.example.com", 443};
}
return state;
} else if constexpr (std::is_same_v<T, Connecting>) {
if (event == "connected") {
return Connected{42, state.host + ":" + std::to_string(state.port)};
} else if (event == "timeout") {
if (state.retry_count > 3) {
return Error{"Max retries exceeded", 1001};
}
auto next = state;
next.retry_count++;
return next;
} else if (event == "cancel") {
return Idle{};
}
return state;
} else if constexpr (std::is_same_v<T, Connected>) {
if (event == "disconnect") {
return Idle{};
} else if (event == "error") {
return Error{"Connection lost", 2001};
}
return state;
} else if constexpr (std::is_same_v<T, Error>) {
if (event == "reset") {
return Idle{};
}
return state;
}
}, current);
}
// 使用
ConnectionState state = Idle{};
state = handle_event(state, "connect"); // Idle -> Connecting
state = handle_event(state, "connected"); // Connecting -> Connected
state = handle_event(state, "disconnect"); // Connected -> Idle
这个方案的优势:
- 编译期完备性检查:
1std::visit
要求处理variant中的每一个类型,漏掉任何状态都会编译报错
- 状态携带数据:每个状态类型可以有不同的数据成员,比传统enum+switch清晰得多
- 无动态分配:
1std::variant
在栈上存储,没有虚函数表指针和堆分配
- 非法状态不可表示:类型系统保证了你不会在Connected状态下访问retry_count
五、C++20 Ranges:重新定义迭代器模式
迭代器模式可能是被现代C++改变最彻底的模式。C++20 Ranges将”遍历+变换+过滤”从手工循环变成声明式管道:
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
26
27
28
29
30
31
32
33
34 #include <ranges>
#include <vector>
#include <string>
#include <algorithm>
// 传统迭代器模式:手工循环
std::vector<std::string> traditional(const std::vector<int>& data) {
std::vector<std::string> result;
for (auto it = data.begin(); it != data.end(); ++it) {
if (*it % 2 == 0 && *it > 10) {
result.push_back(std::to_string(*it * 2));
}
}
return result;
}
// C++20 Ranges:声明式管道
auto modern(const std::vector<int>& data) {
return data
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::filter([](int n) { return n > 10; })
| std::views::transform([](int n) { return std::to_string(n * 2); })
| std::ranges::to<std::vector<std::string>>(); // C++23
}
// Views是惰性的——不触发计算
auto pipeline = std::views::filter([](int n) { return n > 0; })
| std::views::transform([](int n) { return n * n; });
// 此时没有任何计算发生
// 只有在消费时才计算
for (auto val : data | pipeline | std::views::take(5)) {
std::println("{}", val); // 只计算前5个
}
Ranges对迭代器模式的本质改变在于:将”如何遍历”和”遍历时做什么”彻底分离。传统迭代器模式把遍历逻辑和业务逻辑耦合在同一个类中,Ranges通过组合式Views让它们正交化。
六、模式组合:真实项目中的设计模式协作
在真实项目中,设计模式从来不是孤立使用的。以下是一个Web服务器架构的例子,展示多种模式如何协作:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71 #include <expected>
#include <functional>
#include <variant>
#include <memory>
#include <string>
// 请求/响应
struct HttpRequest { std::string method, path, body; };
struct HttpResponse { int status; std::string body; };
// 策略模式:路由策略
using RouteStrategy = std::function<
std::expected<HttpResponse, std::string>(const HttpRequest&)>;
// 装饰器模式:中间件
using Middleware = std::function<RouteStrategy(RouteStrategy)>;
Middleware with_cors() {
return [](RouteStrategy next) -> RouteStrategy {
return [next = std::move(next)](const HttpRequest& req) {
auto result = next(req);
// 添加CORS头
return result;
};
};
}
Middleware with_auth(std::string realm) {
return [realm = std::move(realm)](RouteStrategy next) -> RouteStrategy {
return [next = std::move(next), realm](const HttpRequest& req) {
// 校验Authorization头
if (req.body.find("token:") == std::string::npos) {
return std::unexpected("Unauthorized: " + realm);
}
return next(req);
};
};
}
// 工厂模式:处理器工厂
class HandlerFactory {
public:
RouteStrategy create_user_handler() {
return [](const HttpRequest&) -> std::expected<HttpResponse, std::string> {
return HttpResponse{200, "User data"};
};
}
RouteStrategy create_admin_handler() {
return [](const HttpRequest&) -> std::expected<HttpResponse, std::string> {
return HttpResponse{200, "Admin data"};
};
}
};
// 组合所有模式
class WebServer {
HandlerFactory factory_;
std::vector<Middleware> middlewares_;
public:
void setup() {
// 用户路由:CORS + 认证
auto user_handler = with_cors()(
with_auth("user")(factory_.create_user_handler()));
// 管理路由:CORS + 严格认证
auto admin_handler = with_cors()(
with_auth("admin")(factory_.create_admin_handler()));
}
};
在这个架构中:
- 策略模式定义了路由处理器的统一接口
- 装饰器模式(中间件链)叠加横切关注点
- 观察者模式处理请求事件的异步通知
- 工厂模式封装处理器创建逻辑
- std::expected统一错误处理路径
每种模式各司其职,通过
1 | std::function |
这一通用可调用对象包装器无缝衔接。
七、现代C++下的反模式与取舍
并非所有经典模式在现代C++下都值得保留原始形态。以下是需要重新审视的几个模式:
| 经典模式 | 现代C++替代 | 取舍分析 |
|---|---|---|
| Visitor(双分派) | std::variant + std::visit | 编译期安全,但牺牲了运行时扩展性 |
| Command | Lambda + std::function | 极简,但丢失了命令的历史记录和序列化能力 |
| Template Method | CRTP + Concepts | 编译期多态,零开销,但调试困难 |
| Flyweight | std::shared_ptr + string interning | 更安全,但有原子引用计数开销 |
| Bridge | Pimpl + unique_ptr | 编译防火墙不变,移动语义让Pimpl实现更简单 |
| Memento | 值语义 + std::optional | 状态快照天然支持,但深拷贝大对象仍是问题 |
核心原则:用语言特性替代模式。当一个设计模式的存在仅仅是因为语言缺少某项特性时,语言特性就是更好的解决方案。但当模式背后的设计意图——解耦、扩展性、职责分离——在语言特性之上仍有价值时,模式就应该保留,只是实现方式要随语言进化。
八、总结:设计模式的现代C++准则
经过以上重构实践,我们可以总结出以下准则:
- 优先用Concepts替代虚函数:编译期约束比运行时多态更安全、更快
- 优先用std::variant替代继承体系:当类型集封闭时,variant提供编译期完备性保证
- 优先用std::expected替代异常:对可预期的错误,expected比异常更高效且更显式
- 优先用lambda替代策略子类:策略的本质是”一段可替换的行为”,lambda就是最直接的表达
- 优先用RAII替代手动生命周期管理:观察者的订阅/取消、文件的打开/关闭、锁的获取/释放,全部用RAII
- 保留模式的设计意图,替换实现手段:模式的思想永不过时,过时的是为了弥补语言缺陷而产生的实现细节
设计模式不是教条,而是工具。现代C++给了我们更好的工具,让我们用更少的代码表达同样的设计意图。这才是”现代”的真正含义——不是抛弃经典,而是用更强大的语言能力让经典思想以更优雅的方式落地。
汤不热吧