第一部分:创建型组件(5 个组件)

课程定位

这一部分是整个课程的基石。目标不是教你"什么是设计模式”——你应该已经知道了。目标是用 C++ 编译期能力把模式做成零摩擦组件:一行 #include,一个命名空间,类型安全,零虚函数开销(除非你就是想要动态派发)。

选这 5 个模式的标准:

  • 结构足够简单,一页代码能装下
  • 能展示一种 C++ 封装手法(CRTP、变参模板、Policy、类型擦除)
  • 封装后真正能减少重复代码,不是为封装而封装

读完这一部分,你应该形成一个肌肉记忆:看到一个设计模式,脑子里自动浮现"用模板怎么套”。


1. Singleton — CRTP 泛化单例

意图 & 现实场景

单例是"用过头了"的重灾区,但它该用的时候还是得用:日志系统、全局配置、资源池。痛点在于每个单例类都要重写一遍 getInstance(),还要处理线程安全、析构顺序、禁用拷贝。

用 CRTP(奇异递归模板模式,Curiously Recurring Template Pattern)把这个重复代码提取成基类模板,子类只需要继承 + 传类型名。

实现结构

graph TD
    SG["<b>Singleton&lt;T&gt;</b><br/>CRTP 基类<br/>static T& instance()<br/>禁止拷贝/移动"]
    LOG["<b>Logger</b><br/>继承 Singleton&lt;Logger&gt;<br/>void log(msg)"]
    SG -->|"CRTP 继承"| LOG

核心封装

// patterns/singleton/singleton.hpp
#pragma once

namespace patterns {

/// @brief CRTP Singleton — 继承即可获得全局唯一实例
/// @tparam T 子类自身类型
///
/// 使用方式:
///   class MySingleton : public Singleton<MySingleton> {
///     friend class Singleton<MySingleton>;  // 允许基类访问私有构造
///     MySingleton() = default;
///   public:
///     void doWork();
///   };
///
///   auto& s = MySingleton::instance();  // 线程安全, 惰性初始化
template <typename T>
class Singleton {
public:
    // 局部静态变量(Meyer's Singleton) — C++11 起线程安全
    static T& instance() {
        static T inst;
        return inst;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

protected:
    Singleton() = default;
    ~Singleton() = default;
};

} // namespace patterns

关键注释:

  • static T inst 在 C++11 开始是线程安全的(编译器保证静态局部变量初始化只执行一次),不需要手写双重检查锁定(double-checked locking,十有八九会写出 bug)。
  • 构造/析构放 protected,子类可以通过 friend class Singleton<T> 把构造设成 private。
  • 析构顺序:局部静态变量通常按构造的相反顺序析构(LIFO,后构造的先析构),跨单例依赖时注意——C++ 标准不保证这个顺序,只是大多数编译器这么实现。

进阶:可销毁 + 重新初始化

template <typename T>
class ResettableSingleton {
public:
    static T& instance() {
        if (!inst_) {
            std::call_once(flag_, [] { inst_.reset(new T); });
        }
        return *inst_;
    }

    static void destroy() {
        std::call_once(flag_, [] {});  // 确保 call_once 已完成
        inst_.reset();
    }

    // ... 拷贝移动删除同 Singleton

protected:
    ResettableSingleton() = default;
    ~ResettableSingleton() = default;

private:
    static std::unique_ptr<T> inst_;
    static std::once_flag flag_;
};

template <typename T>
std::unique_ptr<T> ResettableSingleton<T>::inst_;

template <typename T>
std::once_flag ResettableSingleton<T>::flag_;

使用示例

#include "patterns/singleton/singleton.hpp"

class ConfigManager : public patterns::Singleton<ConfigManager> {
    friend class patterns::Singleton<ConfigManager>;
    ConfigManager() {}

public:
    void load(const std::string& path) { /* ... */ }
    std::string get(const std::string& key) const { return {}; }
};

// 使用
auto& cfg = ConfigManager::instance();
cfg.load("/etc/app.json");
std::cout << cfg.get("log_level") << '\n';

常见陷阱

陷阱 说明
跨单例析构依赖 A 的析构函数调 B::instance(),但 B 已经析构了。局部静态变量只是「通常 LIFO」,不保证。解决方案:显式 destroy 控制顺序。
多 DLL/so 每个动态库有自己的静态存储区 → 单例不单。解决方案:单例放在单个 DLL 中导出,或用依赖注入替代单例。
测试隔离 每个测试用例共享同一个单例,状态污染。解决方案:ResettableSingleton,或测试时用依赖注入 mock。
线程安全错觉 局部静态变量初始化是线程安全的,但之后对单例内部成员的并发访问仍需手动加锁同步。

2. Factory — 自注册工厂

意图 & 现实场景

典型的"一坨 if-else 创建对象”:根据配置文件字符串创建对应的 Parser、Compressor、Backend 等。需求来了加一个类,if-else 加一个分支。忘了加 → 运行时报错。

自注册工厂:新类写好后,一行宏自动注册到工厂表,零代码修改工厂本身。核心手法是 CRTP + 静态成员初始化

实现结构

graph TD
    FAC["<b>Factory&lt;Base,Key,Args&gt;</b><br/>+ registerType(key, creator)<br/>+ create(key, args...)<br/>- map 注册表"]
    REG["<b>AutoRegister&lt;Base,Derived&gt;</b><br/>静态成员初始化<br/>main() 前自动注册"]
    JP["<b>JsonParser</b><br/>具体派生类"]
    FAC -.->|"注册到"| REG
    REG -->|"CRTP 继承"| JP

核心封装

// patterns/factory/factory.hpp
#pragma once

#include <functional>
#include <memory>
#include <string>
#include <unordered_map>

namespace patterns {

template <typename Base, typename Key = std::string, typename... Args>
class Factory {
public:
    using Creator = std::function<std::unique_ptr<Base>(Args...)>;

    static Factory& instance() {
        static Factory f;
        return f;
    }

    /// @brief 手动注册(用于非自注册场景)
    bool registerType(const Key& key, Creator creator) {
        return registry_.emplace(key, std::move(creator)).second;
    }

    /// @brief 创建对象,key 不存在返回 nullptr
    std::unique_ptr<Base> create(const Key& key, Args... args) const {
        auto it = registry_.find(key);
        if (it == registry_.end()) return nullptr;
        return it->second(std::forward<Args>(args)...);
    }

    /// @brief 列出所有已注册 key
    std::vector<Key> keys() const {
        std::vector<Key> result;
        for (auto& [k, _] : registry_) result.push_back(k);
        return result;
    }

private:
    Factory() = default;
    std::unordered_map<Key, Creator> registry_;
};

/// @brief 自注册辅助 — 派生类继承此模板即可自动入册
template <typename Base, typename Derived, typename Key = std::string>
class AutoRegister {
protected:
    static bool registered_;  // 在 .cpp 或头文件中定义

    AutoRegister() {
        (void)registered_;  // 确保静态成员被实例化
    }
};

} // namespace patterns

工厂本身是 Singleton 工厂——整个工厂只存一份注册表。AutoRegister 通过静态成员 registered_ 的初始化触发注册。


自注册宏

// patterns/factory/autoregister.hpp
#pragma once
#include "factory.hpp"

/// @brief  在派生类的头文件中使用此宏完成自注册
/// @param base     基类
/// @param derived  派生类
/// @param key      注册用 key(字符串)
#define PATTERNS_REGISTER(base, derived, key)                    \
    template <typename B, typename D, typename K>                \
    bool patterns::AutoRegister<B, D, K>::registered_ = [] {    \
        return patterns::Factory<base>::instance().registerType( \
            key,                                                 \
            [](auto&&... args) -> std::unique_ptr<base> {       \
                return std::make_unique<derived>(                \
                    std::forward<decltype(args)>(args)...);      \
            });                                                  \
    }()

#endif

这个宏的价值:一行代码替代「派生类名 → if-else 分支」的映射。lambda 立即执行,返回值赋给静态 bool 变量 → 变量初始化在 main() 之前完成。

使用示例

// image_codec.hpp — 基类
struct ImageCodec {
    virtual ~ImageCodec() = default;
    virtual std::vector<uint8_t> decode(std::span<const uint8_t> data) = 0;
    virtual std::vector<uint8_t> encode(std::span<const uint8_t> pixels,
                                        int w, int h) = 0;
};

// png_codec.hpp — 自注册
#include "patterns/factory/autoregister.hpp"

struct PngCodec : public ImageCodec,
                  public patterns::AutoRegister<ImageCodec, PngCodec> {
    std::vector<uint8_t> decode(std::span<const uint8_t> data) override { /* ... */ }
    std::vector<uint8_t> encode(std::span<const uint8_t> pixels,
                                int w, int h) override { /* ... */ }
};

PATTERNS_REGISTER(ImageCodec, PngCodec, "png");  // ← 一行注册

// main.cpp — 动态创建
auto codec = patterns::Factory<ImageCodec>::instance().create("png");
if (codec) {
    auto pixels = codec->decode(fileData);
}

常见陷阱

陷阱 说明
静态初始化顺序 多个 .cpp 文件的 static 变量初始化顺序不确定。工厂本身用函数内静态变量(Singleton)来保证「首次使用时已初始化」。我们的 Factory::instance() 做到了。
静态库中未引用 链接器可能丢弃未被引用的 .o 文件中的静态初始化。解决方案:CMake OBJECT 库或用 WHOLE_ARCHIVE 链接选项。
Key 冲突 两个派生类注册同一个 key → emplace 返回 false。建议使用编译期字符串 hash 或用宏拼接 __FILE__
构造参数差异 Args... 是模板参数,所有派生类必须接受相同的构造签名。如果有不同构造参数,用 ProtoType 模式代替。

3. Builder — 流式 API + 变参模板

意图 & 现实场景

构造一个带 20 个可选参数的 HTTP 请求对象。C++ 没有命名参数,所以用 Builder 模式提供链式调用:

auto req = HttpRequest::builder()
    .url("https://api.example.com")
    .method("POST")
    .header("Content-Type", "application/json")
    .body(json)
    .timeout(std::chrono::seconds(5))
    .build();

关键的 C++ 手法:Builder 不是简单的 setter 链,而是用 move 语义保证零拷贝 + 编译期类型检查

实现结构

graph LR
    BLD["<b>HttpRequest::Builder</b><br/>+ url(str) → Builder&<br/>+ method(str) → Builder&<br/>+ header(k,v) → Builder&<br/>+ build() && → HttpRequest"]
    REQ["<b>HttpRequest</b><br/>- url, method, headers<br/>- body, timeout<br/>构造器为 private"]
    BLD -->|"build() &&<br/>move 语义"| REQ

核心封装

// patterns/builder/builder.hpp
#pragma once

#include <string>
#include <vector>
#include <chrono>
#include <utility>

namespace patterns {

class HttpRequest {
public:
    class Builder;

    // 只读访问接口
    const std::string& url() const { return url_; }
    const std::string& method() const { return method_; }

private:
    // 私有构造:只能通过 Builder::build() 创建
    explicit HttpRequest(Builder&& builder);

    std::string url_;
    std::string method_;
    std::vector<std::pair<std::string, std::string>> headers_;
    std::string body_;
    std::chrono::milliseconds timeout_{30000};
};

class HttpRequest::Builder {
public:
    // --- 每个 setter 返回 *this, 支持链式调用 ---
    Builder& url(std::string val)     { url_ = std::move(val);      return *this; }
    Builder& method(std::string val)  { method_ = std::move(val);   return *this; }
    Builder& header(std::string k, std::string v) {
        headers_.emplace_back(std::move(k), std::move(v));
        return *this;
    }
    Builder& body(std::string val)     { body_ = std::move(val);    return *this; }
    Builder& timeout(std::chrono::milliseconds t) { timeout_ = t;   return *this; }

    /// @brief 构建最终对象,Builder 自身被 move
    /// 限定为 &&: 只能对右值调用,防止 build() 后继续使用 Builder
    HttpRequest build() && {
        return HttpRequest(std::move(*this));
    }

private:
    friend class HttpRequest;
    std::string url_;
    std::string method_;
    std::vector<std::pair<std::string, std::string>> headers_;
    std::string body_;
    std::chrono::milliseconds timeout_{30000};
};

// 实现
inline HttpRequest::HttpRequest(Builder&& b)
    : url_(std::move(b.url_))
    , method_(std::move(b.method_))
    , headers_(std::move(b.headers_))
    , body_(std::move(b.body_))
    , timeout_(b.timeout_)
{}

} // namespace patterns

关键注释:

  • build() && — 右值引用限定。只能 Builder().url(...).build()std::move(builder).build(),不能写完 build() 后又调 setter。编译期就能拦住误用。
  • Builder 字段用 std::move 移入目标对象,避免深度拷贝。
  • 私有构造 + friend Builder → 强制走 Builder 路径。

进阶:编译期必填字段检查

经典 Builder 的最大问题:忘记调 .url() → 运行时才发现字段为空。C++ 可以在编译期解决:

// 思路:用模板参数跟踪"已设置"状态
template <bool HasUrl, bool HasMethod>
class HttpRequestBuilder;

template <>
class HttpRequestBuilder<false, false> {
    // 只暴露 url/method setter, 不暴露 build()
public:
    auto url(std::string val) { /* 迁移到 HasUrl=true 状态 */ }
    auto method(std::string val) { /* 迁移到 HasMethod=true 状态 */ }
};

template <>
class HttpRequestBuilder<true, true> {
    // 暴露 build() + 可选 setter
public:
    HttpRequest build() &&;
};

但这在实际工程中太啰嗦(N 个字段 → 2^N 个特化)。实用折中:在 build() 中用 assert + exception 做运行时校验,配合 CI 测试覆盖。

使用示例

// 最简用法
auto req = patterns::HttpRequest::builder()
    .url("https://api.example.com/data")
    .method("GET")
    .header("Accept", "application/json")
    .timeout(std::chrono::seconds(10))
    .build();

// 这就是你不会犯错的原因: 编译错误
// auto req = builder.build(); builder.url("foo"); // build() 后 Builder 被 move 走了

常见陷阱

陷阱 说明
build() 后继续用 build() && 从编译期阻断。如果为了灵活性不限定,至少把 build() 设为 && 语义明确。
拷贝开销 Builder 字段应该是值类型(string, vector),build 时 move 出去。不要存指针/引用 → 悬垂。
过于复杂 字段超过 8 个考虑拆成子 Builder,或者干脆用聚合初始化 + designated initializers (C++20)。
和 Prototype 混淆 Builder 是"分步骤创建复杂对象”,Prototype 是"克隆已有对象”。Builder 的中间产物(Builder 对象)本身也可以是 Prototype。

4. Strategy — Policy-Based Design

意图 & 现实场景

GoF 的 Strategy 模式用接口 + 派生类实现算法切换,运行时灵活但有虚函数开销。C++ 的做法:Policy-Based Design —— 算法作为模板参数,编译期绑定,零开销。

场景:一个 Logger 类,输出到 stdout、文件、syslog——三种"策略”。传统做法:

class ILogSink { virtual void write(string) = 0; };
class StdoutSink : public ILogSink { ... };
class FileSink : public ILogSink { ... };
Logger(unique_ptr<ILogSink> sink); // 虚函数调用 per 日志行

Policy-Based:

Logger<StdoutSink> logger;  // 编译期绑定, 零开销

实现结构

graph TD
    LOG["<b>Logger&lt;OutputPolicy, FormatPolicy&gt;</b><br/>+ log(level, msg)<br/>+ info / warn / error<br/>编译期策略组合"]
    OUT["<b>StdoutPolicy / FilePolicy</b><br/>+ write(msg)<br/>私有继承 · 空基类优化"]
    FMT["<b>RawFormat</b><br/>+ format(level, msg) → string<br/>静态调用"]
    LOG -->|"私有继承<br/>EBO 零开销"| OUT
    LOG -.->|"静态方法调用"| FMT

Policy 示例:

  • StdoutPolicy — write() 到 stdout
  • FilePolicy — write() 到文件
  • RotatePolicy — 按大小滚动

核心封装

// patterns/strategy/strategy.hpp
#pragma once

#include <string>
#include <string_view>
#include <mutex>
#include <cstdio>

namespace patterns {

// ====== Policy: 输出目标 ======
struct StdoutPolicy {
    void write(std::string_view msg) {
        std::fwrite(msg.data(), 1, msg.size(), stdout);
        std::fputc('\n', stdout);
    }
};

struct StderrPolicy {
    void write(std::string_view msg) {
        std::fwrite(msg.data(), 1, msg.size(), stderr);
        std::fputc('\n', stderr);
    }
};

struct FilePolicy {
    explicit FilePolicy(const std::string& path) {
        file_ = std::fopen(path.c_str(), "a");
    }
    ~FilePolicy() { if (file_) std::fclose(file_); }

    FilePolicy(const FilePolicy&) = delete;      // FILE* 不可拷贝
    FilePolicy& operator=(const FilePolicy&) = delete;

    void write(std::string_view msg) {
        if (file_) {
            std::fwrite(msg.data(), 1, msg.size(), file_);
            std::fputc('\n', file_);
        }
    }
private:
    std::FILE* file_ = nullptr;
};

// ====== Policy: 格式化 ======
struct RawFormat {
    static std::string format(std::string_view level, std::string_view msg) {
        std::string result;
        result.reserve(msg.size() + 32);
        result += '[';
        result += level;
        result += "] ";
        result += msg;
        return result;
    }
};

struct TimestampedFormat {
    static std::string format(std::string_view level, std::string_view msg);
    // 实现返回 "[2026-05-10 12:34:56] [INFO] msg"
};

// ====== 组合 ======
/// @brief 策略型 Logger
/// @tparam OutputPolicy 输出策略 (StdoutPolicy / FilePolicy / ...)
/// @tparam FormatPolicy 格式化策略 (RawFormat / TimestampedFormat / ...)
template <typename OutputPolicy = StdoutPolicy,
          typename FormatPolicy = RawFormat>
class Logger : private OutputPolicy {  // 空基类优化: 无额外内存
public:
    using OutputPolicy::OutputPolicy;  // 继承构造函数

    void log(std::string_view level, std::string_view msg) {
        auto formatted = FormatPolicy::format(level, msg);
        std::lock_guard lock(mutex_);
        this->write(formatted);  // 调用 OutputPolicy::write
    }

    void info(std::string_view msg)  { log("INFO", msg); }
    void warn(std::string_view msg)  { log("WARN", msg); }
    void error(std::string_view msg) { log("ERROR", msg); }

private:
    std::mutex mutex_;
};

// ====== 类型别名 ======
using StdoutLogger = Logger<StdoutPolicy>;
using FileLogger   = Logger<FilePolicy>;
using ConsoleLogger = Logger<StderrPolicy, TimestampedFormat>;

} // namespace patterns

关键注释:

  • class Logger : private OutputPolicy — 用私有继承来获得 OutputPolicy 的方法。当 OutputPolicy 是空类时(如 StdoutPolicy 不含任何成员变量),编译器会优化掉 Logger 因继承多出来的内存占用(空基类优化,EBO)。
  • Policy 的 write / format 是鸭子类型(duck typing)而非虚函数接口。任何有 write(std::string_view) 方法的类都是合法的 OutputPolicy,编译器只关心「有没有这个方法」而不关心类型名。这就是 C++20 concepts 的用武之地。
  • 线程安全:互斥锁放在 Logger 层,策略本身不需要管同步。

带 Concepts 约束的版本(C++20)

#include <concepts>

template <typename T>
concept OutputPolicyConcept = requires(T& t, std::string_view msg) {
    { t.write(msg) } -> std::same_as<void>;
};

template <typename T>
concept FormatPolicyConcept = requires(std::string_view level, std::string_view msg) {
    { T::format(level, msg) } -> std::convertible_to<std::string>;
};

template <OutputPolicyConcept OP = StdoutPolicy,
          FormatPolicyConcept FP = RawFormat>
class Logger { /* ... */ };

Concepts 把鸭子类型的 error 从 3 页模板实例化报错收缩成 1 行:“T does not satisfy OutputPolicyConcept”。

使用示例

// 编译期选择策略
patterns::StdoutLogger logger1;
logger1.info("Hello");

patterns::FileLogger logger2("/var/log/app.log");
logger2.error("Something went wrong");

// 自定义策略: 上传到网络
struct NetworkPolicy {
    void write(std::string_view msg) { /* HTTP POST */ }
};
patterns::Logger<NetworkPolicy, patterns::TimestampedFormat> cloudLogger;
cloudLogger.info("This goes to the cloud");

Strategy vs Policy-Based 对照

维度 传统 Strategy (GoF) Policy-Based (C++)
绑定时机 运行时(虚函数) 编译期(模板)
开销 vtable 查找 + 间接调用 零开销,可内联
切换 运行时换 Strategy 对象 编译期不同模板实例
灵活性 可运行时切换 实例化后不可变
何时用 需要运行时切换算法 编译期已知、性能敏感

常见陷阱

陷阱 说明
模板膨胀(代码体积) 不同的 Policy 组合会生成不同的模板实例,导致编译产物变大。折中:把 Policy 无关的公共逻辑抽成非模板基类。
报错难读 不用 concept 时,Policy 接口写错会导致几百行的模板实例化错误信息。C++20 用 concept 可以把错误缩短到一行,C++17 可以用 static_assert 手动做类型检查。
构造参数传递 using OutputPolicy::OutputPolicy 只能完美转发基类的所有构造。如果 Logger 想用自己的构造要另外写。
虚函数必要时的退化 Logger 是编译期绑定的,但你可以包装一层运行时派发:std::function<void(string_view)> 做 OutputPolicy → 这就是 Observer 模式了(见下一节)。

5. Observer — 类型擦除信号槽

意图 & 现实场景

观察者模式在 C++ 里的经典化身是信号槽(Qt 的 signals/slots、Boost.Signals2)。我们要做一个 header-only 版本:类型安全、支持 lambda、自动断开、线程安全可选。

关键 C++ 手法:类型擦除——把 void(T) 的可调用对象(lambda、函数指针、成员函数 bind、std::function)擦成统一类型存起来,调用时还原。

实现结构

graph TD
    SIG["<b>Signal&lt;Args...&gt;</b><br/>+ connect(cb) → Connection<br/>+ emit(args...)<br/>+ disconnect_all()<br/>类型擦除 · shared_mutex"]
    CONN["<b>Connection</b><br/>+ disconnect()<br/>+ connected() → bool<br/>RAII 句柄"]
    SLOT["<b>SlotImpl&lt;Fn&gt;</b><br/>类型擦除存储<br/>lambda / function / bind"]
    SIG -->|"返回"| CONN
    SIG -->|"持有"| SLOT
    CONN -.->|"共享状态"| SLOT

Connection 是 RAII 句柄——调用 disconnect() 后回调自动从 Signal 移除,Connection 对象析构时也会自动断开。

核心封装

// patterns/observer/observer.hpp
#pragma once

#include <functional>
#include <memory>
#include <mutex>
#include <vector>
#include <shared_mutex>

namespace patterns {

// ====== Connection 句柄 ======
class Connection;

namespace detail {

struct SlotState {
    bool active = true;
    std::mutex mtx;
};

// 类型擦除基类
struct SlotBase {
    std::shared_ptr<SlotState> state = std::make_shared<SlotState>();
    virtual ~SlotBase() = default;
};

template <typename Fn>
struct SlotImpl : SlotBase {
    Fn fn;
    explicit SlotImpl(Fn f) : fn(std::move(f)) {}
};

} // namespace detail

class Connection {
public:
    Connection() = default;
    Connection(std::shared_ptr<detail::SlotState> state)
        : state_(std::move(state)) {}

    void disconnect() {
        if (state_) {
            std::lock_guard lock(state_->mtx);
            state_->active = false;
        }
    }

    bool connected() const {
        if (!state_) return false;
        std::lock_guard lock(state_->mtx);
        return state_->active;
    }

private:
    std::shared_ptr<detail::SlotState> state_;
};

// ====== Signal ======
template <typename... Args>
class Signal {
public:
    /// @brief 连接回调,返回 Connection 句柄
    /// 支持: lambda, std::function, 函数指针, std::bind 结果
    template <typename Callable>
    Connection connect(Callable&& cb) {
        using Fn = std::decay_t<Callable>;
        auto slot = std::make_shared<detail::SlotImpl<Fn>>(
            std::forward<Callable>(cb));
        auto conn = Connection(slot->state);

        std::lock_guard lock(mutex_);
        slots_.push_back(std::move(slot));
        return conn;
    }

    /// @brief 发射信号,所有活跃回调依次调用
    void emit(Args... args) {
        std::shared_lock lock(mutex_);
        for (auto& slot : slots_) {
            {
                std::lock_guard slot_lock(slot->state->mtx);
                if (!slot->state->active) continue;
            }

            auto& impl = static_cast<detail::SlotImpl<void(Args...)>&>(*slot);
            try {
                impl.fn(std::forward<Args>(args)...);
            } catch (...) {}
        }
    }

    /// @brief 断开所有连接
    void disconnect_all() {
        std::lock_guard lock(mutex_);
        for (auto& slot : slots_) {
            std::lock_guard slot_lock(slot->state->mtx);
            slot->state->active = false;
        }
        slots_.clear();
    }

    /// @brief 安全版: 调用 emit 时允许在回调中修改订阅列表
    void emit_safe(Args... args) {
        std::vector<std::shared_ptr<detail::SlotBase>> snapshot;
        {
            std::shared_lock lock(mutex_);
            snapshot = slots_;
        }
        for (auto& slot : snapshot) {
            {
                std::lock_guard sl(slot->state->mtx);
                if (!slot->state->active) continue;
            }
            auto& impl = static_cast<detail::SlotImpl<void(Args...)>&>(*slot);
            try { impl.fn(std::forward<Args>(args)...); } catch (...) {}
        }
    }

private:
    mutable std::shared_mutex mutex_;
    std::vector<std::shared_ptr<detail::SlotBase>> slots_;
};

} // namespace patterns

使用示例

#include "patterns/observer/observer.hpp"

// 定义一个信号
patterns::Signal<int, int> onData;

// 连接 lambda
auto c1 = onData.connect([](int x, int y) {
    std::cout << "Sum: " << x + y << '\n';
});

// 连接成员函数
struct Handler {
    void handle(int x, int y) { std::cout << x * y << '\n'; }
};
Handler h;
auto c2 = onData.connect([&h](int x, int y) { h.handle(x, y); });

// 发射
onData.emit(3, 4);  // 输出: Sum: 7 /n 12

// 断开
c1.disconnect();
onData.emit(5, 6);  // 只有 c2 收到

常见陷阱

陷阱 说明
回调中 disconnect 自身 emit() 中执行回调 → 回调中 disconnect() 当前 connection → 因为状态检查在调用前,不会影响当前调用,但下次 emit 不会触发。emit_safe 先拷贝 snapshot 再遍历,更安全。
悬挂引用 lambda 捕获了已析构对象的引用。Connection 不能防止这个。用 std::enable_shared_from_this 或捕获 std::weak_ptr
回调抛异常 默认静默吞掉。生产代码应加入异常日志或提供错误处理回调。
递归 emit A 的回调 emit B,B 的回调 emit A → 死循环。生产代码加调用深度限制。
性能 每个 slot 是 shared_ptr + 间接调用。高频 emit 考虑直接手写列表或参考 Boost.Signals2。

第一部分总结

五个组件,五种 C++ 封装手法:

组件 C++ 手法 一句话
Singleton CRTP + 局部静态变量 继承即单例,线程安全,零代码重复
Factory CRTP + 静态初始化 一行宏注册,永不漏加 if-else 分支
Builder Move 语义 + && 限定 链式调用,build 后不可用
Strategy Policy-Based Design 编译期算法组合,零虚函数开销
Observer 类型擦除 + shared_mutex lambda/member/bind 全兼容,安全断开

仓库产出

完成这一部分后,以下目录应当存在:

cpp-patterns/
├── include/patterns/
│   ├── singleton/singleton.hpp
│   ├── factory/factory.hpp
│   ├── factory/autoregister.hpp
│   ├── builder/builder.hpp
│   ├── strategy/strategy.hpp
│   └── observer/observer.hpp
├── examples/
│   ├── 01_singleton.cpp
│   ├── 02_factory.cpp
│   ├── 03_builder.cpp
│   ├── 04_strategy.cpp
│   └── 05_observer.cpp
└── tests/
    ├── test_singleton.cpp
    ├── test_factory.cpp
    ├── test_builder.cpp
    ├── test_strategy.cpp
    └── test_observer.cpp

下一步

第二部分(结构型模式)和第三部分(行为型模式)将在此基础上扩展:

  • Adapter / Bridge / Decorator → 更多模板组合技巧
  • State / Command / Visitor → variant + visit 现代 C++ 替代
  • Active Object / Thread Pool → 并发基础设施

参考文档

本课程大量使用了以下 C++ 组件化手法,如有不熟悉的读者可以先阅读这几篇基础文章:


文档版本: v1.0 | 日期: 2026-05-10 | 作者: Logic