C++ Bridge Pattern Complete Guide
이 글의 핵심
C++ Bridge pattern separates implementation (Implementor) and abstraction (Abstraction) to enable swappable platforms and drivers, with practical examples, renderer switching, and platform-independent design.
What is Bridge Pattern? Why Needed?
In the Structural Patterns Series, viewing Bridge alongside Adapter and Composite makes it easy to distinguish where “separating abstraction and implementation” applies.
Problem Scenario: Combinatorial Explosion
Problem: Combining Shape (circle, rectangle) and Renderer (OpenGL, Vulkan) with inheritance causes class explosion.
// Bad design: combinatorial explosion
class OpenGLCircle : public Shape { };
class VulkanCircle : public Shape { };
class OpenGLRectangle : public Shape { };
class VulkanRectangle : public Shape { };
// 3 Shapes × 2 Renderers = 6 classes
// Adding Color: 3 × 2 × 3 = 18...
Solution: Bridge pattern separates abstraction (Shape) and implementation (Renderer). Shape only references Renderer, allowing independent extension of both.
// Good design: Bridge
class Shape {
protected:
std::shared_ptr<Renderer> renderer_;
public:
explicit Shape(std::shared_ptr<Renderer> r) : renderer_(std::move(r)) {}
virtual void draw() = 0;
};
class Circle : public Shape {
void draw() override { renderer_->drawCircle(...); }
};
// Adding Shape: 1 class only, Adding Renderer: 1 class only
flowchart LR
client[Client]
abstraction[Abstractionbr/(Shape)]
implementor[Implementorbr/(Renderer)]
refined["RefinedAbstractionbr/(Circle, Rectangle)"]
concrete["ConcreteImplementorbr/(OpenGLRenderer, VulkanRenderer)"]
client --> abstraction
abstraction --> implementor
refined -.extends.-> abstraction
concrete -.implements.-> implementor
Table of Contents
- Basic Structure
- Renderer Switching Example
- Platform-Independent Design
- Common Problems and Solutions
- Production Patterns
- Complete Example: Cross-Platform Window System
1. Basic Structure
#include <memory>
#include <iostream>
// Implementation interface (Implementor)
class Renderer {
public:
virtual void drawCircle(float x, float y, float r) = 0;
virtual void drawRect(float x, float y, float w, float h) = 0;
virtual ~Renderer() = default;
};
// Concrete implementation 1
class OpenGLRenderer : public Renderer {
public:
void drawCircle(float x, float y, float r) override {
std::cout << "[OpenGL] Circle at (" << x << "," << y << ") r=" << r << '\n';
}
void drawRect(float x, float y, float w, float h) override {
std::cout << "[OpenGL] Rect at (" << x << "," << y << ") " << w << "x" << h << '\n';
}
};
// Concrete implementation 2
class VulkanRenderer : public Renderer {
public:
void drawCircle(float x, float y, float r) override {
std::cout << "[Vulkan] Circle at (" << x << "," << y << ") r=" << r << '\n';
}
void drawRect(float x, float y, float w, float h) override {
std::cout << "[Vulkan] Rect at (" << x << "," << y << ") " << w << "x" << h << '\n';
}
};
// Abstraction - references implementation
class Shape {
protected:
std::shared_ptr<Renderer> renderer_;
public:
explicit Shape(std::shared_ptr<Renderer> r) : renderer_(std::move(r)) {}
virtual void draw() = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
float x_, y_, r_;
public:
Circle(std::shared_ptr<Renderer> r, float x, float y, float radius)
: Shape(std::move(r)), x_(x), y_(y), r_(radius) {}
void draw() override { renderer_->drawCircle(x_, y_, r_); }
};
class Rectangle : public Shape {
float x_, y_, w_, h_;
public:
Rectangle(std::shared_ptr<Renderer> r, float x, float y, float w, float h)
: Shape(std::move(r)), x_(x), y_(y), w_(w), h_(h) {}
void draw() override { renderer_->drawRect(x_, y_, w_, h_); }
};
int main() {
auto gl = std::make_shared<OpenGLRenderer>();
auto vk = std::make_shared<VulkanRenderer>();
Circle c1(gl, 0, 0, 10);
Circle c2(vk, 5, 5, 3);
Rectangle r1(gl, 10, 10, 50, 30);
c1.draw(); // [OpenGL] Circle
c2.draw(); // [Vulkan] Circle
r1.draw(); // [OpenGL] Rect
return 0;
}
2. Renderer Switching Example
Runtime Renderer Change
#include <memory>
#include <iostream>
class Renderer {
public:
virtual void render(const std::string& content) = 0;
virtual ~Renderer() = default;
};
class HTMLRenderer : public Renderer {
public:
void render(const std::string& content) override {
std::cout << "<html><body>" << content << "</body></html>\n";
}
};
class MarkdownRenderer : public Renderer {
public:
void render(const std::string& content) override {
std::cout << "# " << content << "\n";
}
};
class Document {
protected:
std::shared_ptr<Renderer> renderer_;
std::string content_;
public:
Document(std::shared_ptr<Renderer> r, std::string content)
: renderer_(std::move(r)), content_(std::move(content)) {}
void setRenderer(std::shared_ptr<Renderer> r) {
renderer_ = std::move(r);
}
virtual void display() = 0;
virtual ~Document() = default;
};
class Article : public Document {
public:
using Document::Document;
void display() override {
std::cout << "=== Article ===\n";
renderer_->render(content_);
}
};
int main() {
auto html = std::make_shared<HTMLRenderer>();
auto md = std::make_shared<MarkdownRenderer>();
Article article(html, "Hello World");
article.display(); // HTML rendering
article.setRenderer(md);
article.display(); // Markdown rendering
return 0;
}
Key: Can switch implementation at runtime with setRenderer().
3. Platform-Independent Design
Cross-Platform File System
#include <memory>
#include <iostream>
#include <string>
// Implementation: platform-specific file operations
class FileSystemImpl {
public:
virtual bool exists(const std::string& path) = 0;
virtual std::string read(const std::string& path) = 0;
virtual void write(const std::string& path, const std::string& data) = 0;
virtual ~FileSystemImpl() = default;
};
class WindowsFileSystem : public FileSystemImpl {
public:
bool exists(const std::string& path) override {
std::cout << "[Windows] Checking: " << path << '\n';
return true;
}
std::string read(const std::string& path) override {
return "[Windows] File content";
}
void write(const std::string& path, const std::string& data) override {
std::cout << "[Windows] Writing to " << path << '\n';
}
};
class LinuxFileSystem : public FileSystemImpl {
public:
bool exists(const std::string& path) override {
std::cout << "[Linux] Checking: " << path << '\n';
return true;
}
std::string read(const std::string& path) override {
return "[Linux] File content";
}
void write(const std::string& path, const std::string& data) override {
std::cout << "[Linux] Writing to " << path << '\n';
}
};
// Abstraction: platform-independent API
class File {
protected:
std::shared_ptr<FileSystemImpl> fs_;
std::string path_;
public:
File(std::shared_ptr<FileSystemImpl> fs, std::string path)
: fs_(std::move(fs)), path_(std::move(path)) {}
bool exists() { return fs_->exists(path_); }
std::string read() { return fs_->read(path_); }
void write(const std::string& data) { fs_->write(path_, data); }
};
class ConfigFile : public File {
public:
using File::File;
void load() {
if (exists()) {
std::cout << "Config loaded: " << read() << '\n';
}
}
};
int main() {
#ifdef _WIN32
auto fs = std::make_shared<WindowsFileSystem>();
#else
auto fs = std::make_shared<LinuxFileSystem>();
#endif
ConfigFile config(fs, "/etc/app.conf");
config.load();
return 0;
}
Key: Select platform implementation at compile time, abstraction layer provides same API.
4. Common Problems and Solutions
Problem 1: Circular Dependency
// ❌ Bad: Renderer references Shape
class Renderer {
std::vector<Shape*> shapes_; // Circular dependency!
};
Solution: Implementation (Implementor) should not know about abstraction (Abstraction). Maintain unidirectional dependency only.
// ✅ Good: Only Shape references Renderer
class Shape {
std::shared_ptr<Renderer> renderer_; // Unidirectional
};
Problem 2: Implementation Leak
// ❌ Bad: Abstraction depends on concrete type
class Circle : public Shape {
OpenGLRenderer* gl_; // Concrete type!
};
Solution: Abstraction should only know Implementor interface.
// ✅ Good
class Circle : public Shape {
std::shared_ptr<Renderer> renderer_; // Interface only
};
Problem 3: Unnecessary Bridge
Bridge is excessive for simple cases.
// ❌ Over-engineering: Only 1 implementation
class Logger {
std::shared_ptr<LoggerImpl> impl_; // Unnecessary
};
Solution: Use Bridge only when 2+ implementations needed or platform/driver switching expected.
Summary
| Item | Description |
|---|---|
| Purpose | Separate abstraction and implementation for independent extension |
| Advantages | Easy platform/driver switching, prevent inheritance explosion, runtime implementation change |
| Disadvantages | Increased class count, design complexity, can be excessive for simple cases |
| When to Use | 2+ platforms/renderers/drivers, runtime switching needed, prevent combinatorial explosion |
| Related: Adapter Pattern, Decorator Pattern, Proxy Pattern, Strategy Pattern, Facade Pattern. | |
| One-line summary: Bridge pattern enables independent extension and runtime switching of renderers, platforms, and drivers. |
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Complete C++ Bridge pattern guide. Structural pattern that separates implementation (Implementor) and abstraction (Abstr… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Facade 패턴 완벽 가이드 | 복잡한 서브시스템을 하나의 간단한 인터페이스로
- C++ Composite 패턴 완벽 가이드 | 트리 구조를 동일 인터페이스로 다루기
- C++ Bridge Pattern Complete Guide | Enhancing Scalability by Separating Implementation and Abstraction
- C++ 디자인 패턴 | Adapter·Decorator
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ Decorator Pattern 완벽 가이드 | 기능 동적 추가와 조합
이 글에서 다루는 키워드 (관련 검색어)
C++, Bridge, design pattern, structural, abstraction, implementation, renderer 등으로 검색하시면 이 글이 도움이 됩니다.