欢迎光临

C++设计模式的现代实践:用C++17/20/23重写23种经典模式

设计模式是面向对象编程的基石,但许多经典教材中的实现仍停留在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

这个方案的优势:

  • 编译期完备性检查
    1
    std::visit

    要求处理variant中的每一个类型,漏掉任何状态都会编译报错

  • 状态携带数据:每个状态类型可以有不同的数据成员,比传统enum+switch清晰得多
  • 无动态分配
    1
    std::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++给了我们更好的工具,让我们用更少的代码表达同样的设计意图。这才是”现代”的真正含义——不是抛弃经典,而是用更强大的语言能力让经典思想以更优雅的方式落地。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » C++设计模式的现代实践:用C++17/20/23重写23种经典模式
分享到: 更多 (0)