作者: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 类型,又是一轮。

更好的做法: 把「能记录日志」这件事拆成两个东西:

  1. 一个规范的内部接口concept 或虚基类)——你自己的代码只依赖它
  2. 一个适配器——把任何满足签名的东西塞进来

这就叫类型擦除适配器

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&lt;Impl&gt; 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) 横切关注点

三个原则:

  1. 先接口、后代理。 所有结构型模式的基础是抽象出一个统一接口
  2. 模板优先于虚函数。 但只在接口稳定的路径上用模板
  3. 测量,别猜。 每个 benchmark 都跑真实数据——性能优化没有直觉,只有数字

下一篇:第三部分:行为型组件 — Observer/Strategy/State/Visitor/Command 的 C++ 现代化实现

上一篇:第一部分:基础工具箱 — CRTP、自注册工厂、Builder move 语义、Policy-Based Design、类型擦除信号槽

参考: