桥接模式的核心是组合加接口指针,而非继承;抽象层通过std::unique_ptr<Implementor>持有实现,运行时动态切换,避免编译期耦合与头文件依赖。

桥接模式的核心不是继承,是组合 + 接口指针
桥接模式在 C++ 里最常被误写成“用虚函数多态替代继承”,结果还是紧耦合。真正起作用的是:把实现细节抽成独立的 Implementor 类族,抽象层(Abstraction)只持有一个指向它的 std::unique_ptr<Implementor> 或裸指针——运行时才能换实现,编译期不绑定。
常见错误现象:
• 抽象类里直接定义 ConcreteImplementorA 成员,导致头文件强依赖
• Abstraction 的构造函数硬编码 new 某个具体实现,失去可替换性
• 忘记将 Implementor 的析构函数设为 virtual,造成资源泄漏
- 抽象类只声明行为接口(如
draw()、resize()),不关心怎么画 - 实现类族各自封装平台/算法细节(如
Win32Renderer、OpenGLRenderer),继承自纯虚基类Renderer - 抽象类通过组合持有
std::unique_ptr<Renderer>,构造时传入具体实现对象 - 如果实现类需要共享状态,用
std::shared_ptr,但要注意循环引用风险
避免头文件爆炸:Pimpl + forward declaration 是刚需
桥接天然带来跨模块依赖,Abstraction.h 不该 include 任何 ConcreteImplementor*.h。否则改一个渲染后端,所有用到 Shape 的地方全得重编译。
正确做法是:在 Abstraction.h 中只前向声明 class Renderer;,把 std::unique_ptr<Renderer> 当作不透明句柄;具体 new 哪个实现,挪到 Abstraction.cpp 里做。
立即学习“C++免费学习笔记(深入)”;
-
Abstraction.h里禁止出现#include "OpenGLRenderer.h" - 用
pimpl惯例包装实现指针(例如struct Impl;+std::unique_ptr<Impl> pimpl_;),进一步隔离变更 - 如果实现类构造开销大,考虑工厂函数返回
std::unique_ptr<Renderer>,而非在抽象类构造中直接 new - 注意:MSVC 对前向声明 +
std::unique_ptr的支持要求Renderer的析构函数必须可见——所以通常把析构定义放在Abstraction.cpp里
动态切换实现时,别忽略资源清理和状态一致性
桥接模式的价值之一是运行时换实现(比如从软件渲染切到 Vulkan)。但直接 impl_ = std::make_unique<VulkanRenderer>() 很危险——旧实现持有的纹理、缓冲区没释放,新实现初始化又可能失败。
典型错误:
• 没检查 new_impl->init() 返回值,就强行赋值
• 切换后没同步当前尺寸、颜色等状态参数到新实现
• 多线程环境下未加锁,impl_ 被并发读写
- 封装一个
setRenderer(std::unique_ptr<Renderer> new_impl)方法,在里面先调用旧实现的teardown(),再调用新实现的init() - 切换前后显式调用
syncStateTo(*new_impl),把抽象层的状态(如宽高、透明度)推过去 - 若需线程安全,用
std::atomic<std::unique_ptr<Renderer>>(C++20 起支持)或加std::mutex,但注意别让锁成为瓶颈 - 不要在析构函数里调用虚函数切换实现——此时虚表已开始销毁,行为未定义
std::variant 替代桥接?别被现代语法带偏
有人想用 std::variant<Win32Renderer, OpenGLRenderer> 代替桥接,看起来更“值语义”。但它本质是编译期确定类型,无法动态增删实现,且每次访问都要 std::visit,破坏了桥接“对扩展开放”的初衷。
性能影响明显:
• std::variant 占用空间是最大实现类型的 size + 一字节 tag,而指针固定 8 字节
• 每次调用都要访存 + 分支预测,虚函数调用反而更缓存友好(vtable 指针常驻 L1)
- 桥接适合实现种类多、生命周期长、需运行时配置的场景(如 GUI toolkit 渲染后端)
-
std::variant更适合实现数量极少(≤3)、类型完全已知、且不希望堆分配的场合(如解析器状态机) - 混合使用也行:用桥接管理大块实现,内部小逻辑用
std::variant做策略选择,但别倒过来 - 别为了用
std::variant而把Renderer设计成无虚函数的 POD——那桥接就退化成 if-else 魔术字符串分发了
桥接最难的从来不是写代码,是判断哪些东西真该拆出去当实现,哪些只是参数差异。比如“抗锯齿开关”该进 Renderer 接口,还是作为 Abstraction 的成员变量传进去——这个边界,文档不会告诉你,得看第一次改需求时哪个文件改得最多。











