第一部分:创建型组件(5 个组件)
课程定位
这一部分是整个课程的基石。目标不是教你"什么是设计模式”——你应该已经知道了。目标是用 C++ 编译期能力把模式做成零摩擦组件:一行 #include,一个命名空间,类型安全,零虚函数开销(除非你就是想要动态派发)。
选这 5 个模式的标准:
- 结构足够简单,一页代码能装下
- 能展示一种 C++ 封装手法(CRTP、变参模板、Policy、类型擦除)
- 封装后真正能减少重复代码,不是为封装而封装
读完这一部分,你应该形成一个肌肉记忆:看到一个设计模式,脑子里自动浮现"用模板怎么套”。
1. Singleton — CRTP 泛化单例
意图 & 现实场景
单例是"用过头了"的重灾区,但它该用的时候还是得用:日志系统、全局配置、资源池。痛点在于每个单例类都要重写一遍 getInstance(),还要处理线程安全、析构顺序、禁用拷贝。
用 CRTP(奇异递归模板模式,Curiously Recurring Template Pattern)把这个重复代码提取成基类模板,子类只需要继承 + 传类型名。
实现结构
graph TD
SG["<b>Singleton<T></b><br/>CRTP 基类<br/>static T& instance()<br/>禁止拷贝/移动"]
LOG["<b>Logger</b><br/>继承 Singleton<Logger><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<Base,Key,Args></b><br/>+ registerType(key, creator)<br/>+ create(key, args...)<br/>- map 注册表"]
REG["<b>AutoRegister<Base,Derived></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<OutputPolicy, FormatPolicy></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() 到 stdoutFilePolicy— 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<Args...></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<Fn></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++ 组件化手法,如有不熟悉的读者可以先阅读这几篇基础文章:
- C++ 组件化基础:模板、CRTP、Policy-Based Design、类型擦除 — 四块基石的详细介绍和实例
- C++20 Concepts:如何约束组件接口 — 用 concept 替代鸭子类型报错
- 编译期 vs 运行期:何时用哪种 — 决策框架与两层混合设计
- 组件契约:concept + static_assert + 编译期测试 — 三层防御体系守住组件质量
文档版本: v1.0 | 日期: 2026-05-10 | 作者: Logic