Bridge Pattern
- Description: Decouple an abstraction hierarchy from its implementation hierarchy so the two can vary independently — composition instead of a combinatorial inheritance tree.
- My Notion Note ID: K2C-2-7
- Created: 2026-05-22
- Updated: 2026-05-22
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. The Problem
- 2. Structure
- 3. C++ Example
- 4. When to Use
- 5. Variants and Pitfalls
- 6. Related Patterns
- 7. References
1. The Problem
Two orthogonal dimensions of variation → naive inheritance produces a class explosion.
- Shapes × renderers (Circle, Square, Triangle) × (OpenGL, Vulkan, SVG) → 9 classes.
- Add one shape → +3 classes. Add one renderer → +N shapes.
- Diamond also lurks if both dimensions share a base.
Bridge → factor one dimension behind an Implementor interface. Abstraction holds a pointer to Implementor. Each subtree grows independently → N + M classes, not N × M.
Also the canonical answer to pimpl (private-impl) idiom — same shape, narrower purpose (compile-firewall).
2. Structure
| Role | Responsibility |
|---|---|
| Abstraction | High-level interface. Holds Implementor* (or unique_ptr). |
| RefinedAbstraction | Subclass adding higher-level operations. |
| Implementor | Interface for the implementation hierarchy — usually low-level primitives. |
| ConcreteImplementor | Platform / backend / variant-specific implementation. |
Crucially → Abstraction and Implementor are two independent inheritance chains wired together by composition.
3. C++ Example
3.1 Renderer × Shape
#include <memory>
#include <iostream>
#include <utility>
// Implementor — low-level drawing primitives
class Renderer {
public:
virtual ~Renderer() = default;
virtual void draw_circle(double x, double y, double r) = 0;
virtual void draw_rect(double x, double y, double w, double h) = 0;
};
// ConcreteImplementors
class OpenGLRenderer : public Renderer {
public:
void draw_circle(double, double, double r) override {
std::cout << "GL circle r=" << r << "\n";
}
void draw_rect(double, double, double w, double h) override {
std::cout << "GL rect " << w << "x" << h << "\n";
}
};
class SVGRenderer : public Renderer {
public:
void draw_circle(double x, double y, double r) override {
std::cout << R"(<circle cx=")" << x << R"(" cy=")" << y
<< R"(" r=")" << r << R"("/>)" << "\n";
}
void draw_rect(double x, double y, double w, double h) override {
std::cout << R"(<rect x=")" << x << R"(" y=")" << y
<< R"(" width=")" << w << R"(" height=")" << h << R"("/>)" << "\n";
}
};
// Abstraction — owns a Renderer
class Shape {
public:
explicit Shape(std::shared_ptr<Renderer> r) : renderer_(std::move(r)) {}
virtual ~Shape() = default;
virtual void draw() const = 0;
protected:
std::shared_ptr<Renderer> renderer_;
};
// RefinedAbstractions
class Circle : public Shape {
public:
Circle(std::shared_ptr<Renderer> r, double x, double y, double rad)
: Shape(std::move(r)), x_(x), y_(y), rad_(rad) {}
void draw() const override { renderer_->draw_circle(x_, y_, rad_); }
private:
double x_, y_, rad_;
};
class Square : public Shape {
public:
Square(std::shared_ptr<Renderer> r, double x, double y, double s)
: Shape(std::move(r)), x_(x), y_(y), s_(s) {}
void draw() const override { renderer_->draw_rect(x_, y_, s_, s_); }
private:
double x_, y_, s_;
};
int main() {
auto gl = std::make_shared<OpenGLRenderer>();
auto svg = std::make_shared<SVGRenderer>();
Circle(gl, 1, 2, 3).draw();
Square(svg, 4, 5, 6).draw();
}
Adding Triangle → one class. Adding MetalRenderer → one class. No N×M blowup.
3.2 Pimpl as a Bridge
// widget.h
#include <memory>
class Widget {
public:
Widget();
~Widget(); // declared, defined in .cpp where Impl is complete
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
void render();
private:
struct Impl; // forward declaration
std::unique_ptr<Impl> p_;
};
// widget.cpp
#include "widget.h"
#include <heavy_third_party_header.h>
struct Widget::Impl { /* fields, helpers */ };
Widget::Widget() : p_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
void Widget::render() { /* use *p_ */ }
- Pimpl = Bridge with a degenerate Abstraction hierarchy (one class) and a hidden Implementor → primary motive is compile-time decoupling and ABI stability, not runtime variation.
- Special-member functions must be defined in the .cpp where
Implis complete — otherwiseunique_ptr's deleter instantiation fails.
4. When to Use
Use when:
- Two orthogonal dimensions of variation, both expected to grow.
- Need to switch implementation at runtime (different rendering backend, different DB driver).
- Hiding implementation details / heavy includes → use Pimpl form for ABI stability.
- Avoiding permanent binding between abstraction and implementation (DLL boundary, plugin systems).
Avoid when:
- Only one dimension varies → use plain inheritance or Strategy.
- Implementation never changes → indirection is dead weight.
- Hot path with tight loops → virtual call through Implementor pointer is real cost; consider templates.
5. Variants and Pitfalls
- Template / static Bridge.
template<class Impl> class Shape : private Impl { ... };— compile-time selection, no virtual cost. Loses runtime swapping. - Pimpl. Degenerate Bridge for compile-firewall / ABI hiding.
- Shared Implementor. Multiple Abstractions share one Implementor (cache, GL context). Use
shared_ptror reference, not value.
Pitfalls:
- Confusing direction. Implementor is not a strategy / behavior swap — it's the low-level half. Abstraction is the high-level half. Mixing the two roles defeats the pattern.
- Lifetime. Abstraction owning Implementor by raw pointer → ownership ambiguous. Pick
unique_ptr(sole owner) orshared_ptr(shared). - Compile times for templates. Static Bridge → header-only, blows up TU sizes.
- Pimpl special-member traps. Defaulting destructor / move in header with
unique_ptr<Impl>and an incompleteImpl→ compile error. Define them in the .cpp.
6. Related Patterns
- Bridge vs Adapter. Adapter is retrofitted to glue two already-existing incompatible interfaces. Bridge is designed up front to keep abstraction and implementation independent. Same diagram, different intent and timing.
- Bridge vs Strategy. Strategy swaps one behavior / algorithm. Bridge separates an entire implementation hierarchy from an entire abstraction hierarchy. Strategy = one-axis; Bridge = two-axis structural separation.
- Bridge vs Abstract Factory. Abstract Factory often creates the matched (Abstraction, Implementor) pair — they complement each other.
- Bridge vs Pimpl. Pimpl is the C++-specific compile-firewall reading of Bridge. Same mechanics; goals differ (hiding vs orthogonal variation).
- State. State objects often plug in via a Bridge-shaped pointer.
7. References
- GoF — Design Patterns, Bridge ch.
- refactoring.guru — Bridge
- sourcemaking — Bridge
- Herb Sutter — GotW #100/#101, Pimpl and special members
- cppreference —
std::unique_ptr