Adapter Pattern
- Description: Wrap an incompatible interface so client code can talk to it through the interface it already expects — object-adapter via composition or class-adapter via multiple inheritance.
- My Notion Note ID: K2C-2-6
- 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. Intent
- 2. Structure
- 3. C++ Example
- 4. When to Use
- 5. Variants and Pitfalls
- 6. Related Patterns
- 7. References
1. Intent
- Bridge mismatched interfaces without modifying either side.
- Classic use → legacy lib has API
A, client expects APIB→ Adapter sits between, exposesB, delegates toA. - Also called Wrapper.
Two flavors (GoF):
- Object adapter → holds adaptee by composition (pointer / reference / value). Most common in modern C++.
- Class adapter → inherits from both target interface and adaptee (multiple inheritance). Compile-time, no indirection cost, but rigid.
2. Structure
| Role | Responsibility |
|---|---|
| Target | Interface the client expects (B). |
| Adaptee | Existing class with the incompatible interface (A). |
| Adapter | Implements Target, holds (or inherits) Adaptee, translates calls. |
| Client | Depends only on Target. Unaware of Adaptee. |
Object adapter → Adapter has-a Adaptee.
Class adapter → Adapter is-a Target and is-a Adaptee.
3. C++ Example
Scenario → modern app expects IRenderer::draw(Shape), legacy lib exposes LegacyDraw::render_xy(int, int, int, int).
3.1 Object Adapter (composition)
#include <memory>
#include <iostream>
// Target — what the client expects
struct Shape { int x, y, w, h; };
class IRenderer {
public:
virtual ~IRenderer() = default;
virtual void draw(const Shape& s) = 0;
};
// Adaptee — incompatible legacy API
class LegacyDraw {
public:
void render_xy(int x1, int y1, int x2, int y2) {
std::cout << "legacy: " << x1 << "," << y1
<< " -> " << x2 << "," << y2 << "\n";
}
};
// Adapter — composes the adaptee
class LegacyRendererAdapter : public IRenderer {
public:
explicit LegacyRendererAdapter(std::shared_ptr<LegacyDraw> impl)
: impl_(std::move(impl)) {}
void draw(const Shape& s) override {
impl_->render_xy(s.x, s.y, s.x + s.w, s.y + s.h);
}
private:
std::shared_ptr<LegacyDraw> impl_;
};
void client(IRenderer& r) { r.draw({10, 20, 100, 50}); }
int main() {
auto adapter = LegacyRendererAdapter{std::make_shared<LegacyDraw>()};
client(adapter);
}
3.2 Class Adapter (multiple inheritance)
class LegacyRendererClassAdapter : public IRenderer, private LegacyDraw {
public:
void draw(const Shape& s) override {
render_xy(s.x, s.y, s.x + s.w, s.y + s.h);
}
};
privateinheritance fromLegacyDraw→ adapterIS-ATarget but onlyIMPLEMENTED-IN-TERMS-OFAdaptee.- Cannot adapt a
finaladaptee. Cannot adapt at runtime — picked at compile time.
3.3 Free-function / lambda adapter
For one-shot adaptation of a single call → wrap in a lambda. Cheaper than a class.
auto draw = [adaptee = LegacyDraw{}](const Shape& s) mutable {
adaptee.render_xy(s.x, s.y, s.x + s.w, s.y + s.h);
};
Useful with std::function, range adaptors, callbacks.
4. When to Use
Use when:
- Reusing a class whose interface doesn't match what the surrounding code expects.
- Wrapping a C library behind a C++ interface.
- Adapting third-party SDK to a domain abstraction (so swapping the SDK later is one-file change).
- Conforming several incompatible classes to a common interface so they can be used polymorphically.
Avoid when:
- You control both sides → just fix the interface.
- The "adapter" is doing real work beyond translation → that's a Facade or domain wrapper, not Adapter.
- Translation requires runtime state machines, retries, caching → consider Decorator or a separate service layer.
5. Variants and Pitfalls
- Two-way adapter → implements both interfaces, lets either side talk to either. Rarely needed.
- Pluggable adapter → adaptee chosen at runtime via constructor injection. Default for testability.
- Static / template adapter → CRTP or concept-based, compile-time dispatch. Zero virtual cost.
Pitfalls:
- Lifetime ownership. Adapter holding a raw pointer to an adaptee owned elsewhere → dangling. Prefer
shared_ptr/unique_ptror by-value. - Leaky abstraction. Adapter exposes adaptee-specific error codes / types → client now coupled to adaptee anyway.
- Adapter explosion. N targets × M adaptees = N×M adapters. Sign that the underlying abstraction is wrong.
- Class-adapter MI hazards. Diamond problems if both bases share an ancestor; harder to mock; cannot adapt
finalclasses. - Over-adapting trivial calls. A single function call doesn't justify a class — use a lambda.
6. Related Patterns
- Adapter vs Facade. Adapter exposes the same shape of operations through a different interface → 1-to-1 translation. Facade introduces a new, simpler interface over a subsystem of many classes — interfaces deliberately differ. Facade is about scope reduction; Adapter is about interface translation.
- Adapter vs Decorator. Decorator preserves the wrapped interface — same
Targetin and out, adds behavior. Adapter changes the interface, adds little or no behavior. - Adapter vs Proxy. Proxy keeps the same interface as the subject, controls access (lazy load, remote, permissions). Adapter changes the interface.
- Adapter vs Bridge. Bridge is designed up front to separate abstraction from implementation; Adapter is retrofitted after the fact to fit two existing pieces together.
- Strategy. Adapter often wraps a Strategy when the strategy interface doesn't match the third-party algorithm's API.
7. References
- GoF — Design Patterns: Elements of Reusable Object-Oriented Software, Adapter ch.
- refactoring.guru — Adapter
- sourcemaking — Adapter
- cppreference — Inheritance,
std::function