C++如何实现桥接模式?(解耦抽象与实现)

2次阅读

桥接模式的核心是组合加虚函数,通过abstraction持有implementor指针多态调用,而非继承;implementor须为抽象基类,具体实现仅继承它;指针类型由生命周期归属决定,避免shared_ptr;接口需稳定,新增功能优先用默认实现。

C++如何实现桥接模式?(解耦抽象与实现)

桥接模式的核心不是继承,是组合加虚函数

桥接模式在 c++ 里最容易被写成“用继承模拟组合”——比如让 Abstraction 继承 Implementor,或者反过来。这完全违背桥接本意。它要解决的是抽象层(如 Shape)和实现层(如 Renderer)各自独立变化的问题,靠的是「持有指针 + 多态调用」,不是类层级的耦合。

关键判断:只要你在 Abstraction 里看到 public Implementor 或者 class Abstraction : public Implementor,就错了。

  • Abstraction 类里必须包含一个指向 Implementor 的指针(通常是 std::unique_ptr<implementor></implementor> 或裸指针,视生命周期而定)
  • Implementor 必须是抽象基类,定义纯虚函数(如 virtual void draw() = 0
  • 所有具体实现(OpenGLRendererSkiaRenderer)只继承 Implementor,不碰 Abstraction 体系
  • 构造 Abstraction 子类时,必须传入一个具体的 Implementor 实例,不能在内部 new

std::unique_ptr 还是 raw pointer?看谁管生命周期

桥接对象的生命周期归属决定指针类型——这不是风格选择,是责任划分问题。常见错误是 Abstraction 拿着裸指针却擅自 delete,或用 std::shared_ptr 引入不必要的循环引用。

  • 如果 Abstraction 完全拥有 Implementor(比如用户传进来一个临时对象,你希望它活到 Abstraction 销毁),用 std::unique_ptr<implementor></implementor>
  • 如果 Implementor 是全局单例或由外部长期管理(如 GUI 框架提供的 canvas),用裸指针 + 注释说明“non-owning”,且禁止在析构中 delete
  • 避免 std::shared_ptr:桥接不涉及共享所有权,加 refcount 只会模糊职责,还可能因循环引用导致泄漏

示例:Circle 构造时接收 std::unique_ptr<renderer>&&</renderer>,移动语义保证所有权清晰;而 UIWidget 接收 Renderer* 并标注 // owned by AppContext

立即学习C++免费学习笔记(深入)”;

虚函数表开销小,但别在 hot path 上间接调用

桥接必然引入一次虚函数调用(renderer->draw()),在图形渲染等高频路径上,这比直接内联调用多 1–2 个周期。不是性能瓶颈,但容易被忽略。

  • 不要在每像素循环里调用 implementor->put_pixel();应把整块数据交给 Implementor 批处理(如 render_buffer(const uint8_t*, size_t)
  • 如果某类 Implementor 确实需要极致性能(如 SIMD 渲染),可提供非虚的模板接口(template<typename t> void fast_draw(T&&)</typename>),但桥接主干仍走虚函数,保持扩展性
  • 编译器通常能对单实现场景做 devirtualization,但别依赖——确保关键路径不卡在虚调用上

容易漏掉的:Implementor 接口变更会破坏所有子类

桥接解耦的是类结构,不是接口契约。Implementor 基类一旦增加新纯虚函数,所有派生类都得改,否则链接失败。这点比继承更隐蔽,因为改动发生在“底层”。

  • Implementor 接口设计得足够稳定:优先用组合已有接口(如 Drawable + Resizable),而不是大而全的基类
  • 新增能力尽量用默认实现(C++11 起支持 virtual void save_to_file() { throw std::runtime_error("not implemented"); }),而非强制重写
  • 测试时别只测 Abstraction 行为,要 mock 不同 Implementor 的行为差异,尤其验证异常路径下 Abstraction 是否正确转发错误

真正麻烦的从来不是写对桥接结构,而是当 Renderer 需要支持 Vulkan 后端时,你发现 OpenGLRendererSkiaRenderer 都得补 3 个新虚函数——这时候才意识到,接口粒度早该按能力切分,而不是按技术

text=ZqhQzanResources