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

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 Impl is complete — otherwise unique_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_ptr or 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) or shared_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 incomplete Impl → 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