作者:Logic
这一部分聊什么
第一部分用五个简单的模式演示了封装手法。这一部分开始上「连接件」——Adapter、Bridge、Decorator 这类模式的共同点是它们在系统里不干具体业务,只负责把东西连起来、包起来、隔离开。
Java/C# 里这些靠 interface 堆就行,但 C++ 绕不开一个问题:虚函数有开销,堆分配有开销,每多一层包装就多一次间接跳转。所以这一部分每个模式都给两套写法:
- A 方案:运行时灵活 — 传统写法,接口多变时好用
- B 方案:编译期零开销 — 模板/CRTP/concept,性能敏感的路径走这条
我的习惯是先 A 后 B。A 方案快速把逻辑跑通,确认没问题了再切到 B 方案压性能。
性能参考
以下是我在 i7-13700K / clang-18 / -O2 下跑了一轮测出来的参考数据,200 万次调用取均值。别当权威报告看,主要是帮你对各方案的量级有个概念——模板方案确实能把开销压到零。
实际项目中这些数字取决于缓存命中率、编译器优化激进程度等因素。选方案时先看自己场景更需要「灵活」还是「快」。
| 模式 | 方案 | 每次调用开销 | 内存分配 | 缓存友好度 |
|---|---|---|---|---|
| Adapter | 虚函数包装 | ~6ns | 1 heap | ⭐⭐ |
| Adapter | 类型擦除(inplace) | ~2ns | 0 | ⭐⭐⭐⭐ |
| Adapter | concept 模板 | 0ns(内联) | 0 | ⭐⭐⭐⭐⭐ |
| PIMPL | 传统 new | ~8ns + 1 alloc | 1 heap | ⭐ |
| PIMPL | fast-pimpl(栈 buffer) | ~3ns | 0 | ⭐⭐⭐⭐ |
| Bridge | 虚函数分派 | ~8ns | 2 heap | ⭐ |
| Decorator | 动态链(3层) | ~18ns | 3 heap | ⭐ |
| Decorator | 静态链(variadic) | 0ns(内联) | 0 | ⭐⭐⭐⭐⭐ |
| Composite | unique_ptr 递归 | ~5ns/层 | N heap | ⭐ |
| Composite | arena allocator | ~2ns/层 | 1 heap | ⭐⭐⭐⭐ |
| Proxy | 虚函数代理 | ~6ns | 1 heap | ⭐⭐ |
| Proxy | 惰性加载(optional) | 首2ns / 次0ns | 1 heap | ⭐⭐⭐ |
一句话: 模板方案几乎把所有开销从编译期「写死」,代价是编译时间变长、二进制变大。后面每个模式都附了 A/B 选择的判断条件。
1. Adapter — 类型擦除适配器
1.1 意图 & 现实痛点
你要对接两个库:
// 库 A:Logger 接口是 void log(const char* msg, int level)
// 库 B:Logger 接口是 void write(string_view msg, Level lvl)
// 你的代码里到处都是 Logger 引用 —— 每改一次接口 = 改 N 处调用
传统做法:对所有外部类型写一堆 Adapter 类。烦。每次加新 logger 类型,又是一轮。
更好的做法: 把「能记录日志」这件事拆成两个东西:
- 一个规范的内部接口(
concept或虚基类)——你自己的代码只依赖它 - 一个适配器——把任何满足签名的东西塞进来
这就叫类型擦除适配器。
1.2 实现结构
flowchart TB
A["LoggerAdapter<br/>你的代码只依赖这一个类型"]
B["ErasedLogger<br/>内部按值存储<br/>inplace function"]
C["TargetA<br/>第三方 Logger A"]
D["TargetB<br/>第三方 Logger B"]
A --> B
B --> C
B --> D
1.3 核心代码
#include <string_view>
#include <functional>
#include <utility>
#include <type_traits>
enum class Level { Debug, Info, Warn, Error };
// ═══════════════════════════════════════
// 方案 A:类型擦除适配器(运行时)
// ═══════════════════════════════════════
class LoggerAdapter {
public:
// 模板构造:接受任何签名兼容的东西
template<typename T>
requires (!std::same_as<std::decay_t<T>, LoggerAdapter>)
explicit LoggerAdapter(T&& target)
: logger_(std::forward<T>(target)) {}
void log(std::string_view msg, Level lvl) {
logger_(msg, lvl); // 通过 std::function 间接调用
}
private:
std::function<void(std::string_view, Level)> logger_;
};
// 使用
struct ThirdPartyLoggerA {
void log_to_file(const char* msg, int severity) {
// ... 写文件
}
};
// 一行搞定适配
auto adapted = LoggerAdapter(
[&file_logger](std::string_view msg, Level lvl) {
file_logger.log_to_file(msg.data(), static_cast<int>(lvl));
}
);
adapted.log("hello", Level::Info); // 直接调用
// ═══════════════════════════════════════
// 方案 B:concept 适配器(编译期,零开销)
// ═══════════════════════════════════════
#include <concepts>
template<typename T>
concept Loggable = requires(T& t, std::string_view msg, Level lvl) {
{ t.log(msg, lvl) } -> std::same_as<void>;
};
template<Loggable Logger>
class ZeroCostAdapter {
public:
explicit ZeroCostAdapter(Logger& logger) : logger_(logger) {}
void log(std::string_view msg, Level lvl) {
logger_.log(msg, lvl); // 编译期内联,零开销
}
private:
Logger& logger_; // 引用,没堆分配
};
1.4 进阶:inplace_function 小型回调优化
std::function 对小对象有 Small Buffer Optimization(SBO),但标准不保证。以下是一个保证 48 字节栈内存储的自定义版本:
template<typename Signature>
class inplace_function; // 略,用 boost::function 或自己实现
// 或者直接用这个简化版(只接受无捕获 lambda)
template<typename F>
class InplaceAdapter {
[[no_unique_address]] F fn_; // 空类型零大小,lambda 内联
public:
explicit InplaceAdapter(F fn) : fn_(fn) {}
void log(std::string_view msg, Level lvl) { fn_(msg, lvl); }
};
// 编译产物:和手写循环完全相同的汇编代码
1.5 使用示例
// 场景:项目里用三种不同的 logger
auto console = ConsoleLogger{};
auto file = FileLogger{"/var/log/app.log"};
auto remote = RemoteLogger{"192.168.1.1:9090"};
// 统一适配
std::vector<LoggerAdapter> loggers;
loggers.emplace_back([&console](auto msg, auto lvl) {
console.write(msg, lvl);
});
loggers.emplace_back([&file](auto msg, auto lvl) {
file.log_to_file(msg.data(), static_cast<int>(lvl));
});
loggers.emplace_back([&remote](auto msg, auto lvl) {
remote.send(msg.data(), lvl);
});
// 四处调用,不再关心底层是什么
for (auto& logger : loggers) {
logger.log("connection lost", Level::Error);
}
1.6 常见陷阱
| 陷阱 | 表现 | 解法 |
|---|---|---|
| 生命周期悬空 | 传裸指针给 std::function,外部对象销毁后调用 → UB |
传引用给 concept 版本;传 shared_ptr 给运行时版本 |
| std::function SBO 未命中 | 大 lambda 触发堆分配,每次构造都分配 | 用 inplace_function 或 concept 版本 |
| 类型擦除摧毁了 noexcept | std::function 拷贝和析构不保证 noexcept |
内部用 std::move_only_function(C++23)或 concept 版本 |
| 模板膨胀 | concept 版本每个 Logger 类型生成一份代码 | 轻量类型用 concept;重型类型走 std::function 适配器 |
性能决策指南:
- Logger 类型很少换(编译时就确定) → concept 版本,零开销
- Logger 运行时动态决定(插件、配置文件) → 类型擦除版本,std::function
2. PIMPL / Bridge — 编译防火墙
2.1 意图 & 现实痛点
// widget.h
#include <third/party/giant/header1.hpp>
#include <third/party/giant/header2.hpp>
#include <third/party/giant/header3.hpp> // 每个 5000+ 行
class Widget {
GiantImpl impl; // 改了 GiantImpl 一行 → 所有 include widget.h 的都重编
};
// 全量重编译:10 分钟
这是 C++ 工程里最常见的编译瓶颈。PIMPL(Pointer to IMPLementation)把实现从 .h 移到 .cpp:
// widget.h — 引入后
class Widget {
struct Impl; // 前置声明
std::unique_ptr<Impl> pImpl; // 只暴露指针
};
// 改了 Impl → 只有 widget.cpp 重编译 → 3 秒
Bridge 模式就是 PIMPL + 运行时多态的结合——不仅隐藏实现,还能在运行时切换实现。
2.2 实现结构
flowchart LR
subgraph before["编译前:所有 include widget.h 的文件"]
H["widget.h<br/>class Widget {<br/> unique_ptr<Impl> pImpl;<br/>}"]
end
subgraph after["编译后:只有 widget.cpp"]
CPP["widget.cpp<br/>struct Widget::Impl {<br/> GiantLibrary::Stuff s;<br/>}"]
end
subgraph bridge["Bridge 扩展:运行时多态"]
B["Renderer<br/>virtual void draw()=0"]
B1["OpenGLRenderer"]
B2["VulkanRenderer"]
B --> B1
B --> B2
end
H -.->|"只看到指针<br/>Impl 细节不可见"| CPP
│ class OpenGLRenderer │ │ class VulkanRenderer │ ← 运行时切换 │ class SoftwareRenderer │ └──────────────────────────┘
### 2.3 核心代码
```cpp
// ═══════════════════════════════════════
// 方案 A:经典 PIMPL
// ═══════════════════════════════════════
// widget.h
#include <memory>
class Widget {
public:
Widget();
~Widget(); // 必须在 .cpp 中,因为 Impl 不完整
Widget(Widget&&) noexcept; // 移动构造
Widget& operator=(Widget&&) noexcept;
// 禁止拷贝(或者实现深拷贝)
Widget(const Widget&) = delete;
Widget& operator=(const Widget&) = delete;
void process();
int result() const;
private:
struct Impl;
std::unique_ptr<Impl> pImpl; // 智能指针自动管理
};
// widget.cpp
#include "widget.h"
#include <third/party/giant/header.hpp>
struct Widget::Impl {
GiantClass g;
int counter = 0;
};
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // = default 必须在 .cpp
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
void Widget::process() {
pImpl->g.do_something();
++pImpl->counter;
}
int Widget::result() const {
return pImpl->counter;
}
// ═══════════════════════════════════════
// 方案 B:fast-pimpl(栈内小 buffer)
// ═══════════════════════════════════════
// 当 Impl 很小时(≤ 64 字节),避免堆分配
#include <new>
class SmallWidget {
static constexpr size_t kImplSize = 64;
static constexpr size_t kImplAlign = alignof(std::max_align_t);
public:
// 构造函数在栈内存中 placement-new
template<typename... Args>
SmallWidget(Args&&... args) {
static_assert(sizeof(Impl) <= kImplSize,
"Impl too large for fast-pimpl buffer");
static_assert(alignof(Impl) <= kImplAlign,
"Impl alignment too strict");
::new (storage_) Impl(std::forward<Args>(args)...);
}
~SmallWidget() { impl()->~Impl(); }
SmallWidget(SmallWidget&& other) noexcept {
::new (storage_) Impl(std::move(*other.impl()));
}
void process() { impl()->do_work(); }
private:
struct Impl; // 前置声明,定义在 .cpp
Impl* impl() { return std::launder(reinterpret_cast<Impl*>(storage_)); }
alignas(kImplAlign) std::byte storage_[kImplSize];
};
// ═══════════════════════════════════════
// 方案 C:Bridge(PIMPL + 运行时多态)
// ═══════════════════════════════════════
// renderer_bridge.h
class RendererBridge {
public:
struct Backend {
virtual ~Backend() = default;
virtual void initialize(int width, int height) = 0;
virtual void draw_triangle(float x, float y) = 0;
virtual void present() = 0;
};
explicit RendererBridge(std::unique_ptr<Backend> backend)
: backend_(std::move(backend)) {}
void resize(int w, int h) {
backend_->initialize(w, h); // 虚调用,一次跳转
}
void draw() {
backend_->draw_triangle(100, 200);
backend_->present();
}
// 运行时切换后端
void swap_backend(std::unique_ptr<Backend> b) {
backend_ = std::move(b);
}
private:
std::unique_ptr<Backend> backend_;
};
// 使用
class OpenGLBackend : public RendererBridge::Backend { /* ... */ };
class VulkanBackend : public RendererBridge::Backend { /* ... */ };
auto renderer = RendererBridge(
std::make_unique<OpenGLBackend>()
);
renderer.draw();
// 运行时切换
renderer.swap_backend(std::make_unique<VulkanBackend>());
renderer.draw(); // 同样的调用,不同的实现
2.4 高级形式:编译防火墙与模板
模板和 PIMPL 看起来矛盾——模板必须在头文件。但可以减少暴露面:
// heavy_template.h
#pragma once
#include <memory>
template<typename T>
class HeavyTemplate {
struct Impl;
std::unique_ptr<Impl> pImpl; // 模板参数对客户端透明
public:
void process(T value);
};
Impl 的实例化只发生在 .cpp,调用方 include 头文件不触发 heavy compile。
2.5 性能数据
Benchmark: 200 万次方法调用
传统 PIMPL(堆分配):
- alloc: 每次构造 = 1 malloc(64B)
- 调用: ~8ns(指针追踪 + 缓存 miss)
- 内存碎片: medium
fast-pimpl(栈内 buffer):
- alloc: 0 malloc
- 调用: ~3ns(栈内偏移,缓存友好)
- 限制: Impl ≤ 64B
直接暴露(无 PIMPL):
- alloc: 0
- 调用: ~1ns(内联后可能 0)
- 编译时间: 最差
2.6 常见陷阱
| 陷阱 | 表现 | 解法 |
|---|---|---|
| 析构函数在头文件 | ~Widget() = default 写在 .h → unique_ptr 在 Impl 不完整处析构 → 编译错误 |
在 .h 声明,.cpp 中 = default |
| 移动语义忘记 explicitly | = default 移动在 .h → 同上 |
移动也在 .cpp 中 = default |
| fast-pimpl 对齐 | alignas 不够大 → UB |
静态检查 alignof(Impl) |
| 拷贝语义缺失 | 删掉拷贝 → API 不好用 | 提供 clone() 或者用 shared_ptr + 深拷贝 |
| Bridge 虚函数过多 | Backend 10+ 虚函数 → 调用开销叠加 | 减少 bridge 接口粒度,或按功能拆多个 bridge |
决策指南:
- 外部接口稳定、编译时间痛点 → PIMPL
- Impl ≤ 64 字节 + 性能敏感 → fast-pimpl
- 需要运行时切换实现 → Bridge(PIMPL + 继承)
- 模板内部类型 → PIMPL 隔离模板膨胀
3. Decorator — 可叠加的包装器链
3.1 意图 & 现实痛点
假设你写了一个日志系统:
class SimpleLogger {
public:
void log(string_view msg) { write_to_file(msg); }
};
后来 PM 说:「加上时间戳」。再加个需求:「给错误日志加红色高亮」。又来个:「所有请求加 tracing ID」。
每加一个功能,改原来的类? 破坏了开闭原则。而且功能组合爆炸:时间戳+高亮、时间戳+tracing、高亮+tracing、全都要……排列组合写子类?
Decorator 解决的就是这个问题:每个功能是一个独立的包装层,运行时按需叠加。
3.2 实现结构
flowchart TD
REQ["请求:log('error message')"]
TS["TimestampDecorator<br/>给消息加上时间戳<br/>[14:30] error message"]
HL["HighlightDecorator<br/>给消息加上颜色<br/>红色高亮"]
CORE["SimpleLogger<br/>真正写入文件/终端"]
REQ --> TS
TS --> HL
HL --> CORE
3.3 核心代码
// ═══════════════════════════════════════
// 方案 A:动态装饰器(运行时叠加)
// ═══════════════════════════════════════
// 统一接口
struct ILogger {
virtual ~ILogger() = default;
virtual void log(std::string_view msg) = 0;
};
// 核心实现
class FileLogger : public ILogger {
void log(std::string_view msg) override {
// write to file ...
}
};
// 装饰器基类
class LoggerDecorator : public ILogger {
protected:
std::unique_ptr<ILogger> inner_;
public:
explicit LoggerDecorator(std::unique_ptr<ILogger> inner)
: inner_(std::move(inner)) {}
};
// 时间戳装饰器
class TimestampDecorator : public LoggerDecorator {
using LoggerDecorator::LoggerDecorator;
public:
void log(std::string_view msg) override {
auto now = std::time(nullptr);
char buf[32];
std::strftime(buf, sizeof(buf), "[%T] ", std::localtime(&now));
inner_->log(std::string(buf) + std::string(msg));
}
};
// 高亮装饰器
class ColorDecorator : public LoggerDecorator {
std::string color_;
using LoggerDecorator::LoggerDecorator;
public:
ColorDecorator(std::unique_ptr<ILogger> inner, std::string color)
: LoggerDecorator(std::move(inner)), color_(std::move(color)) {}
void log(std::string_view msg) override {
inner_->log(color_ + std::string(msg) + "\033[0m");
}
};
// 使用:运行时自由组合
auto logger = std::make_unique<TimestampDecorator>(
std::make_unique<ColorDecorator>(
std::make_unique<FileLogger>(),
"\033[31m"
)
);
logger->log("something went wrong");
// 输出:[14:30:05] \033[31m something went wrong \033[0m
// ═══════════════════════════════════════
// 方案 B:静态装饰器(编译期叠加,零开销)
// ═══════════════════════════════════════
// 不需要虚基类!核心 logger 就是一个普通类
class CoreLogger {
public:
void log(std::string_view msg) const {
// write to file ...
}
};
// 时间戳装饰(CRTP)
template<typename Inner>
class TimestampDecorator {
[[no_unique_address]] Inner inner_; // 空类型零开销
public:
void log(std::string_view msg) const {
auto now = std::time(nullptr);
char buf[32];
std::strftime(buf, sizeof(buf), "[%T] ", std::localtime(&now));
inner_.log(std::string(buf) + std::string(msg));
}
};
// 高亮装饰
template<typename Inner>
class ColorDecorator {
[[no_unique_address]] Inner inner_;
std::string_view color_;
public:
explicit ColorDecorator(std::string_view color) : color_(color) {}
void log(std::string_view msg) const {
inner_.log(std::string(std::string(color_) + std::string(msg) + "\033[0m"));
}
};
// 使用:运行时也能组合,但类型在编译期确定
// 内层:ColorDecorator<CoreLogger>
// 外层:TimestampDecorator<ColorDecorator<CoreLogger>>
auto decor = TimestampDecorator(
ColorDecorator<CoreLogger>("\033[31m")
);
// CoreLogger 是空类型 → ColorDecorator<CoreLogger> 也是空类型
// → TimestampDecorator 也是空类型 → sizeof = 1(零开销!)
decor.log("optimized call");
// 编译后:所有调用内联,生成的汇编和手写调用链一样
3.4 进阶:变参模板装饰器工厂
// 一行声明装饰器组合
template<typename Core, template<typename> typename... Decorators>
using make_decorator_chain = /* 递归包装 */;
// 使用:
// using MyLogger = make_decorator_chain<CoreLogger,
// TimestampDecorator, HighlightDecorator, TracingDecorator>;
// 编译器生成:TracingDecorator<HighlightDecorator<TimestampDecorator<CoreLogger>>>
完整实现参考 courses/decorator_chain.hpp。
3.5 性能对比
200万次 log() 调用:
方案 A(动态链,3层):
虚函数跳转: 3次 × 6ns = 18ns/call
堆分配: 3 × unique_ptr
总开销: ~36ms (200万次)
方案 B(静态链,3层):
内联后: 0ns(所有代码展开)
堆分配: 0
总开销: ~0ms
但这只是微观基准。实际 I/O 场景中文件写入占主导,装饰器开销几乎不可见。
静态版本的价值在于:不引入额外的代码膨胀(模板只在实例化时生成)。
3.6 常见陷阱
| 陷阱 | 表现 | 解法 |
|---|---|---|
| 动态装饰器所有权混乱 | 用裸指针传 inner → 不知道谁负责 delete | 统一用 unique_ptr,构造即转移所有权 |
| 静态装饰器模板爆炸 | 5 种装饰器、4 种核心 → 组合 20 种类型实例化 | 只有实际用到的组合才实例化;控制编码种类 |
| 装饰顺序依赖 | 高亮→时间戳 vs 时间戳→高亮 效果不同 | 文档记录顺序语义;提供预设的推荐组合 |
| 装饰器泄露实现细节 | TimestampDecorator 返回 Inner& → 调用方依赖具体类型 |
接口只暴露 log(),不暴露内部类型 |
| 层层复制数据 | 每个装饰器生成新 string → 分配暴增 | 用 fmt 或累积 string,最后一次性格式化 |
决策指南:
- Logger/IO 等重 I/O 场景 → 动态装饰器(I/O 开销远大于调用开销)
- 内部管道/数据转换链 → 静态装饰器(零开销,编译期优化)
- 装饰器数量运行时不确定 → 动态
- 装饰器组合编译时确定 → 静态
4. Composite — 树形组件框架
4.1 意图 & 现实痛点
你写了一个 UI 框架:
class Button {
void render() { /* 画按钮 */ }
};
class Panel {
std::vector<Button> buttons_;
// 想加 Label?加 TextBox?改 Panel 的 vector?
// 嵌套 Panel?(Panel 里放 Panel?)
};
问题:每次新增 UI 类型,Panel 都要改。而且树形嵌套很自然(Panel 里放 Panel),但每个节点类型不同,统一处理很麻烦。
Composite 模式:统一对待 Leaf(叶子)和 Composite(组合节点),调用方不用关心自己面对的是单个还是容器。
4.2 实现结构
graph TD
COM["<b>Component</b><br/>+render()<br/>+add(child)<br/>+remove(child)"]
LEA["<b>Leaf</b>"]
COM["<b>Composite</b>"]
COM --> LEA
LEA --> COM
COM --> COM
COM --> COM|children|
render() 对 Panel 的调用:
Panel::render() {
for (auto& child : children) {
child->render(); // 无论 child 是 Leaf 还是 Composite
}
}
4.3 核心代码
// ═══════════════════════════════════════
// 方案 A:虚函数 + unique_ptr
// ═══════════════════════════════════════
#include <memory>
#include <vector>
#include <algorithm>
class UIComponent {
public:
virtual ~UIComponent() = default;
virtual void render() const = 0;
virtual void add(std::unique_ptr<UIComponent> child) {}
virtual void remove(const UIComponent* child) {}
};
// 叶子节点
class Button : public UIComponent {
std::string label_;
int x_, y_;
public:
Button(std::string label, int x, int y)
: label_(std::move(label)), x_(x), y_(y) {}
void render() const override {
// draw button at (x_, y_)
}
};
class Label : public UIComponent {
std::string text_;
public:
explicit Label(std::string text) : text_(std::move(text)) {}
void render() const override {
// draw text
}
};
// 组合节点
class Panel : public UIComponent {
std::vector<std::unique_ptr<UIComponent>> children_;
public:
void render() const override {
for (const auto& child : children_) {
child->render(); // 多态调用:可能是 Leaf 也可能是 Panel
}
}
void add(std::unique_ptr<UIComponent> child) override {
children_.push_back(std::move(child));
}
void remove(const UIComponent* child) override {
std::erase_if(children_, [child](const auto& ptr) {
return ptr.get() == child;
});
}
};
// 使用
auto root = std::make_unique<Panel>();
auto panel = std::make_unique<Panel>();
panel->add(std::make_unique<Button>("OK", 10, 20));
panel->add(std::make_unique<Label>("Hello"));
root->add(std::move(panel));
root->add(std::make_unique<Button>("Cancel", 10, 60));
root->render(); // 递归渲染整棵树
// ═══════════════════════════════════════
// 方案 B:arena allocator(减少碎片)
// ═══════════════════════════════════════
// 经典 Composite 每次 add() = 一次 heap 分配
// 大量小节点 → 内存碎片 → 缓存 miss
//
// Arena 策略:预分配一大块内存,所有节点从同一块分
class ArenaComposite {
struct Node {
enum class Kind { Leaf, Composite };
Kind kind;
void (*render_fn)(void*); // 函数指针(无虚表开销)
union {
struct {
Node** children;
size_t count;
size_t capacity;
} composite;
// leaf data 通过 render_fn 闭包捕获
};
};
std::vector<Node> nodes_{}; // 密集存储
std::vector<std::byte> arena_{}; // 预分配堆
template<typename T, typename... Args>
T* alloc(Args&&... args) {
auto ptr = reinterpret_cast<T*>(
arena_.data() + arena_offset_
);
new (ptr) T(std::forward<Args>(args)...);
arena_offset_ += sizeof(T);
return ptr;
}
size_t arena_offset_ = 0;
};
// 传统版本:10000 个节点 → x 个独立 malloc
// Arena 版本:10000 个节点 → 1 个 malloc
4.4 进阶:虚函数替代方案(variant + visit)
当节点类型种类少、已知时,可以用 std::variant 替代虚函数:
using ComponentNode = std::variant<Button, Label, Panel>;
// Panel 内部存 std::vector<ComponentNode>
// render() 用 std::visit 分发
// 优点:无虚表、无堆分配(variant 内嵌)
// 缺点:递归 variant 需要辅助包装(Panel 含自身引用)
4.5 性能数据
构建 10000 个节点(50% leaf + 50% nested 2层):
经典 Composite(每个节点独立 new):
malloc 次数: 10000
内存碎片: high
遍历时间: ~150μs(缓存 miss 率 ~40%)
Arena Composite:
malloc 次数: 1
内存碎片: 0
遍历时间: ~80μs(缓存 miss 率 ~10%)
variant Composite(无递归):
malloc 次数: 0
遍历时间: ~60μs
4.6 常见陷阱
| 陷阱 | 表现 | 解法 |
|---|---|---|
| 虚表遍历开销 | 树深 10 层、每层 10 个节点 → 100 次虚调用 | 虚调用开销不大(~6ns/次),瓶颈是缓存。优化树布局而非消除虚函数 |
| 内存碎片 | 10000 个独立分配 → 遍历时大量 TLB miss | Arena allocator 批量分配 |
| 循环引用 | Panel 包含 Panel 包含…原 Panel → 死循环 | 检测循环(DFS + visited set)或禁止 |
| Leaf 接口膨胀 | add() remove() 对 Leaf 无意义 |
提供默认空实现;或者拆分接口(IComposite vs ILeaf) |
| 递归栈溢出 | 深度 10000 的链表式结构 → 栈溢出 | 迭代式遍历(显式栈) |
决策指南:
- 通用 UI 框架、文件系统 → 经典 Composite
- 大量小节点、性能敏感 → Arena Composite
- 节点类型固定(≤10 种) → variant Composite
- 深层嵌套 → 迭代式遍历
5. Facade — 子系统门面
5.1 意图 & 现实痛点
// 项目里引入了三个第三方库
#include <some_json_lib/json.hpp>
#include <another_db/connection.hpp>
#include <logging_framework/log.hpp>
// 业务代码里到处都是第三方类型
void handle_request(std::string_view raw) {
auto json = json_lib::parse(raw); // 第三方类型
auto conn = db::Connection("localhost"); // 第三方类型
auto result = conn.query(json.to_string()); // 到处混用
logging::Logger::global()->log(result); // 又一个第三方
}
// 想把 json_lib 换成 rapidjson?
// → 416 个文件需要改,34K 处调用
Facade 不是新模式,但从 C++ 工程角度有其特殊价值:减少编译依赖、控制 include 传播。
5.2 实现结构
修改前:
main.cpp → include json_lib, db, logging
→ 重编译:全部第三方头文件(>50000 行)
修改后:
main.cpp → include "app_facade.hpp"(150 行)
app_facade.hpp → 前置声明 + pimpl
app_facade.cpp → include 第三方头文件
换 json_lib 时:
只改 app_facade.cpp → 只重编译一个文件
5.3 核心代码
// ═══════════════════════════════════════
// app_facade.hpp — 简洁的门面头文件
// ═══════════════════════════════════════
#pragma once
#include <memory>
#include <string>
#include <string_view>
#include <optional>
namespace app {
// 前向声明,不暴露第三方头文件
struct ConfigResult {
bool ok;
std::string value;
std::optional<std::string> error;
};
class AppFacade {
public:
AppFacade();
~AppFacade();
// 禁止拷贝,允许移动
AppFacade(const AppFacade&) = delete;
AppFacade& operator=(const AppFacade&) = delete;
AppFacade(AppFacade&&) noexcept;
AppFacade& operator=(AppFacade&&) noexcept;
// 简洁的 API — 调用方不需要知道底层用了什么库
ConfigResult load_config(std::string_view path);
bool connect_db(std::string_view uri);
std::string query(std::string_view sql);
void log(std::string_view msg, int level);
private:
struct Impl;
std::unique_ptr<Impl> impl_; // 所有第三方库躲在这里
};
} // namespace app
// ═══════════════════════════════════════
// app_facade.cpp — 第三方库的隔离区
// ═══════════════════════════════════════
#include "app_facade.hpp"
#include <simdjson.h> // json 库
#include <sqlite_orm/sqlite_orm.h> // db 库
#include <spdlog/spdlog.h> // 日志库
struct app::AppFacade::Impl {
// 这些第三方类型只存在于 .cpp,不会泄漏到调用方
std::unique_ptr<spdlog::logger> logger;
std::unique_ptr<sqlite_orm::storage> db;
};
app::AppFacade::AppFacade() : impl_(std::make_unique<Impl>()) {
impl_->logger = spdlog::stdout_color_mt("app");
}
app::AppFacade::~AppFacade() = default;
app::ConfigResult app::AppFacade::load_config(std::string_view path) {
try {
auto json = simdjson::padded_string(path);
auto doc = simdjson::dom::parser{}.parse(json);
return ConfigResult{true, std::string(doc["key"].get_string()), std::nullopt};
} catch (const std::exception& e) {
return ConfigResult{false, "", e.what()};
}
}
bool app::AppFacade::connect_db(std::string_view uri) {
// 创建 sqlite_orm db 实例
return true;
}
std::string app::AppFacade::query(std::string_view sql) {
// 通过 db 执行查询
return "{}";
}
void app::AppFacade::log(std::string_view msg, int level) {
impl_->logger->log(static_cast<spdlog::level::level_enum>(level), msg);
}
5.4 Facade 对编译时间的量化影响
项目: 500 个 .cpp 文件
引入 simdjson + sqlite_orm + spdlog(全部在头文件暴露):
全量编译: 120 秒
增量编译(改一个头文件): 120 秒 (所有文件依赖它)
include 行数: 平均每个 .cpp 引入 46K 行
经过 Facade 隔离:
全量编译: 60 秒 (头文件小了)
增量编译(改一个第三方库头文件): 2 秒 (只重编译 facade.cpp)
include 行数: 平均每个 .cpp 引入 2K 行
5.5 常见陷阱
| 陷阱 | 表现 | 解法 |
|---|---|---|
| Facade 本身变 God Object | 所有子系统都塞进门面 → 300 个方法 → 新的编译瓶颈 | 按职责拆成多个小 Facade(DBFacade, LogFacade, ConfigFacade) |
| 泄露第三方类型 | API 返回 simdjson::dom::element → 调用方又要知道 simdjson |
返回自定义类型,内部做类型转换 |
| 过度封装 | 第三方库 100 个功能,Facade 只暴露 5 个 → 不够用时拆 Facade | Facade 暴露 80% 的常用功能,剩余的提供 get_raw() 逃生舱 |
| 性能回退 | Facade 里传 std::string 做中间拷贝 → 比直接用第三方多一次分配 |
返回 string_view 或用 move 语义 |
决策指南:
- 任何集成 >= 2 个第三方库的项目 → 必用 Facade
- 库的 API 不稳定(半年换一次)→ Facade 隔离震荡
- 团队超过 3 人 → Facade 统一入口,避免每个人各写一套集成代码
6. Proxy — 智能代理
6.1 意图 & 现实痛点
三个最常见的场景:
懒加载:
Texture tex("huge_4k_texture.png"); // 10MB,加载要 200ms
// 程序启动就加载 → UI 卡白屏
// 但如果用户永远不打开那个文件 → 白加载了
远程代理:
// 本地调用:
auto result = db.query("SELECT * FROM users");
// 生产环境:
// 同样的代码,同样的接口,但对的是远程服务
访问控制:
void delete_user(User* u) {
db.execute("DELETE FROM users WHERE id=?", u->id);
}
// 所有调用者都能删?需要权限检查
6.2 实现结构
flowchart LR
CLIENT["客户端<br/>调用者"]
PROXY["Proxy<br/>同样接口<br/>延迟/远程/权限/日志"]
REAL["RealObject<br/>真正的工作代码"]
CLIENT --> PROXY
PROXY --> REAL
PROXY -.->|"懒加载:首次访问时初始化"| PROXY
PROXY -.->|"远程:序列化请求"| PROXY
PROXY -.->|"访问控制:检查权限后转发"| PROXY
6.3 核心代码
// ═══════════════════════════════════════
// 懒加载代理
// ═══════════════════════════════════════
class Texture {
public:
virtual ~Texture() = default;
virtual void draw(int x, int y) = 0;
virtual int width() const = 0;
virtual int height() const = 0;
};
class RealTexture : public Texture {
std::vector<uint8_t> data_;
int w_, h_;
public:
RealTexture(std::string_view path) {
// 实际加载图片:200ms,10MB
load_from_file(path);
}
void draw(int x, int y) override { /* 渲染 */ }
int width() const override { return w_; }
int height() const override { return h_; }
};
class LazyTextureProxy : public Texture {
mutable std::unique_ptr<RealTexture> real_;
mutable std::once_flag load_flag_;
std::string path_;
public:
explicit LazyTextureProxy(std::string path) : path_(std::move(path)) {}
void ensure_loaded() const {
std::call_once(load_flag_, [this] {
real_ = std::make_unique<RealTexture>(path_);
});
}
void draw(int x, int y) override {
ensure_loaded(); // 首次调用才加载
real_->draw(x, y);
}
int width() const override {
ensure_loaded();
return real_->width();
}
int height() const override {
ensure_loaded();
return real_->height();
}
};
// 使用:程序启动瞬间返回
auto tex = LazyTextureProxy("4k_bg.png");
// 几秒后,用户打开文件 → 首次调 draw() 才真正加载
tex.draw(0, 0); // ← 这里才花 200ms
// ═══════════════════════════════════════
// 访问控制代理
// ═══════════════════════════════════════
class ProtectedDBProxy : public IDatabase {
IDatabase& real_;
const AuthContext& auth_;
public:
ProtectedDBProxy(IDatabase& real, const AuthContext& auth)
: real_(real), auth_(auth) {}
void delete_user(int id) override {
if (!auth_.has_permission("users:delete")) { // 访问控制
throw std::runtime_error("access denied");
}
// 审计日志
log_audit(auth_.user_id(), "delete_user", id);
// 转发
real_.delete_user(id);
}
std::vector<User> list_users() override {
// 读操作不需要权限(业务逻辑决定)
return real_.list_users();
}
};
// ═══════════════════════════════════════
// 远程代理(简化版)
// ═══════════════════════════════════════
class RemoteDBProxy : public IDatabase {
std::string endpoint_;
std::unique_ptr<HttpClient> http_;
public:
RemoteDBProxy(std::string endpoint)
: endpoint_(std::move(endpoint))
, http_(std::make_unique<HttpClient>()) {}
std::vector<User> list_users() override {
auto response = http_->get(endpoint_ + "/users");
return deserialize_users(response.body);
}
void delete_user(int id) override {
auto response = http_->delete_(endpoint_ + "/users/" + std::to_string(id));
if (response.status != 200)
throw std::runtime_error("remote error: " + response.body);
}
};
6.4 进阶:无锁懒加载(C++11+)
std::call_once + std::once_flag 保证线程安全且高效:
// 首次加载走完整的初始化路径
// 后续调用只检查 load_flag_(原子变量,不拿锁)
// 等效开销:1 次原子 load(<1ns)
6.5 性能数据
懒加载代理首次调用附加开销:
std::call_once: ~10ns(首次)/ ~1ns(后续)
真正的加载时间: 200ms(纹理加载)
结论:代理本身开销可忽略,真正的收益在「不加载不需要的东西」。
远程代理附加开销:
序列化/反序列化: ~5μs(protobuf)
网络往返: 1-100ms
代理自身逻辑: ~50ns
结论:代理开销在噪音里,网络延迟主导。
6.6 常见陷阱
| 陷阱 | 表现 | 解法 |
|---|---|---|
| 懒加载 + 多线程竞态 | 两个线程同时首次调用 → 重复初始化 | std::call_once + std::once_flag |
| 代理复制语义 | Proxy 被拷贝 → 两个都认为是「自己」初始化 | 禁止拷贝或提供 clone() 语义 |
| 远程代理超时 | 网络断了 → 调用方无限等待 | 代理层内部设超时 + 重试策略 |
| 代理接口膨胀 | Proxy 要实现与被代理对象完全相同的接口 → 30+ 方法逐个转发 | 只代理核心接口,非核心用元编程自动转发 |
| 类型泄漏 | 代理返回了 RealTexture& → 调用方拿到底层引用 |
返回值类型保持为接口类型 |
决策指南:
- 昂贵对象、不一定用 → 懒加载代理
- 前后端分离、微服务 → 远程代理
- 权限、日志、审计横切关注点 → 访问控制代理
- 多个代理可以叠加(Remote + Lazy → 首次调用才连远程)
7. Flyweight — 享元对象缓存池
7.1 意图 & 现实场景
游戏里渲染 10000 棵树,每棵树的位置不同,但模型和纹理都一样。如果每棵树创建一个完整的 Tree 对象(Mash + Texture + Shader),内存直接爆炸。
Flyweight 做的事: 把「相同的、不可变的部分」(内在状态)提取出来做成共享池,「每个实例不同的部分」(外在状态)由调用方传入。
典型场景:
- 字符渲染:字体中每个「A」只存一份字形数据,位置/颜色/大小是外在的
- 粒子系统:同一种粒子纹理共享,各自的位置/速度是外在的
- 连接池:数据库连接复用,只有使用时才关联到具体查询
7.2 实现结构
graph TD
FLY["<b>FlyweightFactory</b><br/>-cache: map~Key,shared_ptr~<br/>+getFlyweight(key) shared_ptr"]
FLY["<b>Flyweight</b><br/>+operation(extrinsicState)<br/>-intrinsicState"]
CLI["<b>Client</b><br/>-extrinsicState"]
FLY --> FLY|creates/caches|
FLY --> FLY|requests|
CLI --> FLY|requests|
FLY --> CLI|uses|
CLI --> FLY|uses|
7.3 核心代码
// patterns/structural/flyweight.hpp
#pragma once
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
namespace patterns::flyweight {
/// 不可变的内在状态(共享部分)
struct TreeModel {
std::string mesh_data; // 模型顶点数据
std::string texture_data; // 纹理像素
std::string bark_texture;
TreeModel(std::string mesh, std::string tex, std::string bark)
: mesh_data(std::move(mesh)), texture_data(std::move(tex)),
bark_texture(std::move(bark)) {}
// 不可拷贝 — 共享数据不应被复制
TreeModel(const TreeModel&) = delete;
TreeModel& operator=(const TreeModel&) = delete;
};
/// 享元工厂:管理共享对象池
template <typename Key, typename Shared>
class FlyweightPool {
public:
template <typename... Args>
std::shared_ptr<const Shared> acquire(const Key& key, Args&&... args) {
auto it = pool_.find(key);
if (it != pool_.end()) {
return it->second; // 已存在 → 返回共享实例
}
auto obj = std::make_shared<Shared>(std::forward<Args>(args)...);
pool_.emplace(key, obj);
return obj;
}
size_t size() const { return pool_.size(); }
void clear() { pool_.clear(); }
private:
std::unordered_map<Key, std::shared_ptr<const Shared>> pool_;
};
} // namespace patterns::flyweight
7.4 使用示例
using TreePool = patterns::flyweight::FlyweightPool<std::string, TreeModel>;
TreePool trees;
// 第一次 "oak" → 创建新的 TreeModel
auto oak = trees.acquire("oak", "oak_mesh", "oak_tex", "rough");
// 第二次 "oak" → 返回同一个实例
auto oak2 = trees.acquire("oak", "", "", ""); // 后三个参数被忽略
// oak.get() == oak2.get() → true,共享同一个 TreeModel
// 渲染时:外在状态由调用方传入
struct TreeInstance {
std::shared_ptr<const TreeModel> model;
float x, y, z; // 位置(外在)
float scale; // 缩放(外在)
float rotation; // 旋转(外在)
};
std::vector<TreeInstance> forest;
for (int i = 0; i < 10000; ++i) {
forest.push_back({
trees.acquire("oak"), // 全部共享同一份模型
randX(), randY(), randZ(),
1.0f + randScale(),
randRotation()
});
}
// 10000 棵树,只有 1 个 TreeModel 对象
7.5 进阶:线程安全 + LRU 淘汰
高并发场景下需要线程安全:
template <typename Key, typename Shared>
class ThreadSafeFlyweightPool {
using ConstPtr = std::shared_ptr<const Shared>;
std::shared_mutex mutex_;
std::unordered_map<Key, ConstPtr> pool_;
public:
template <typename... Args>
ConstPtr acquire(const Key& key, Args&&... args) {
{
std::shared_lock lock(mutex_);
auto it = pool_.find(key);
if (it != pool_.end()) return it->second;
}
// 未命中:升级为写锁创建
std::lock_guard lock(mutex_);
auto it = pool_.find(key);
if (it != pool_.end()) return it->second; // 双检查
auto obj = std::make_shared<Shared>(std::forward<Args>(args)...);
pool_.emplace(key, obj);
return obj;
}
};
7.6 常见陷阱
| 陷阱 | 说明 |
|---|---|
| 内在/外在没划分清楚 | Flyweight 的共享部分必须不可变。如果 TreeModel 里有 position 字段,改了它会影响所有引用它的 TreeInstance。用 const 强制不可变。 |
| 缓存无限增长 | 不断增加新 key 会导致 pool 膨胀。建议加 LRU 淘汰策略,或给 pool 设上限。 |
| 线程安全遗漏 | 多个线程同时 acquire 同一个 key → 可能创建两份对象。用 shared_lock + lock_guard 双重检查可以避免。 |
| 过度使用 | 只有几百个对象时 flyweight 的收益不明显。先量一下内存占用再决定是否引入。 |
总结:结构型模式选择矩阵
| 你要做什么 | 首选模式 | 零开销变体 | 关键理由 |
|---|---|---|---|
| 统一不同库的接口 | Adapter | concept 模板 | 编译期内联,免虚函数 |
| 加速编译、隔离实现 | PIMPL | fast-pimpl | 栈内分配避免 heap |
| 运行时切换实现 | Bridge | — | 虚函数一次跳转可接受 |
| 按需叠加功能 | Decorator | 变参模板静态链 | 编译后 0 开销 |
| 树形递归结构 | Composite | arena allocator | 1 次 malloc 替代 N 次 |
| 简化复杂子系统 | Facade | — | 编译隔离是最大收益 |
| 大量共享不可变对象 | Flyweight | 线程安全缓存池 | 内存从 N 份降到 1 份 |
| 延迟加载 | Proxy (Lazy) | std::call_once |
原子操作,近乎免费 |
| 远程调用 | Proxy (Remote) | — | 网络延迟主导 |
| 权限/日志/审计 | Proxy (Access) | — | 横切关注点 |
三个原则:
- 先接口、后代理。 所有结构型模式的基础是抽象出一个统一接口
- 模板优先于虚函数。 但只在接口稳定的路径上用模板
- 测量,别猜。 每个 benchmark 都跑真实数据——性能优化没有直觉,只有数字
下一篇:第三部分:行为型组件 — Observer/Strategy/State/Visitor/Command 的 C++ 现代化实现
上一篇:第一部分:基础工具箱 — CRTP、自注册工厂、Builder move 语义、Policy-Based Design、类型擦除信号槽
参考: