The Adapter Pattern in C++: Object vs Class Adapters for Legacy and Third-Party APIs

Key takeaways

Adapter pattern in C++: object vs class adapter, payment/API examples, unique_ptr—bridge incompatible interfaces for legacy and third-party code.

Adapter is one of the structural patterns, alongside Decorator, Facade, Bridge and Composite. For a JS angle on wrapping APIs, see JavaScript patterns.

Of all the classic design patterns, Adapter is probably the one you already use without naming it. Any time you write a thin class or function that takes a third-party library’s awkward API and presents it in the shape your code wants, you have written an adapter. The value of naming it is in the discipline it suggests: keep the translation in one place, make your code depend on your own interface rather than the vendor’s, and you can replace the vendor, fake it in tests, or survive its next breaking release by changing one class.

What is Adapter Pattern? why you need it

Problem Scenario: Incompatible Interfaces

Problem: The interface of an existing library is not compatible with my code.

// The interface my code expects
class MediaPlayer {
public:
    virtual void play(const std::string& filename) = 0;
};
// Existing library (not compatible)
class VLCPlayer {
public:
    void playVLC(const std::string& filename) { /* ... */ }
};
// How to use VLCPlayer as MediaPlayer?

Solution: Adapter Pattern converts interfaces. Adapter implements the Target interface and calls Adaptee internally.

// Adapter
class VLCAdapter : public MediaPlayer {
public:
    VLCAdapter(std::unique_ptr<VLCPlayer> player)
        : vlc(std::move(player)) {}
    
    void play(const std::string& filename) override {
vlc->playVLC(filename);  // interface conversion
    }
    
private:
    std::unique_ptr<VLCPlayer> vlc;
};

Key Concept: Adapter Pattern acts as a bridge to bridge incompatible interfaces. This is an essential pattern when integrating legacy systems or using third-party libraries.

The vocabulary comes from the Gang of Four book. The Target is the interface your code wants (MediaPlayer), the Adaptee is the existing class with the wrong interface (VLCPlayer), and the Adapter implements the Target by calling the Adaptee. The important property is the direction of dependency: code that uses MediaPlayer has no idea VLC exists. Only VLCAdapter includes the VLC headers, so if you switch media libraries, the change is confined to one file.

An adapter should translate, not add behaviour. Converting names, argument order, units, error-reporting style and ownership conventions is its job. Adding caching, retries or logging on top turns it into a decorator or a service, and mixing the two makes the adapter harder to test and replace. When a translation is lossy (the target supports something the adaptee cannot do), the adapter is also the right place to make that explicit, by throwing or returning an error rather than silently ignoring the request.


index

  1. Object Adapter
  2. Class adapter
  3. Legacy code integration
  4. Frequently occurring problems and solutions
  5. Production Patterns
  6. Complete example: Payment system

object adapter

An object adapter wraps an Adaptee using composition. This is the most common and recommended method.

Combination method

#include <iostream>
#include <memory>
#include <string>
class MediaPlayer {
public:
    virtual void play(const std::string& filename) = 0;
    virtual ~MediaPlayer() = default;
};
class VLCPlayer {
public:
    void playVLC(const std::string& filename) {
        std::cout << "Playing VLC: " << filename << '\n';
    }
};
class MP4Player {
public:
    void playMP4(const std::string& filename) {
        std::cout << "Playing MP4: " << filename << '\n';
    }
};
class VLCAdapter : public MediaPlayer {
public:
    VLCAdapter() : vlc(std::make_unique<VLCPlayer>()) {}
    
    void play(const std::string& filename) override {
        vlc->playVLC(filename);
    }
    
private:
    std::unique_ptr<VLCPlayer> vlc;
};
class MP4Adapter : public MediaPlayer {
public:
    MP4Adapter() : mp4(std::make_unique<MP4Player>()) {}
    
    void play(const std::string& filename) override {
        mp4->playMP4(filename);
    }
    
private:
    std::unique_ptr<MP4Player> mp4;
};
int main() {
    std::unique_ptr<MediaPlayer> player;
    
    player = std::make_unique<VLCAdapter>();
    player->play("movie.vlc");
    
    player = std::make_unique<MP4Adapter>();
    player->play("movie.mp4");
}

Composition means the adapter holds an adaptee rather than being one. That has several practical advantages. The adaptee can be any type, including final classes, classes with no virtual functions, or C structs behind a handle. The adapter can own the adaptee (unique_ptr, as here), share it, or merely borrow a reference to one that someone else manages, and that choice is visible in the member’s type. And the adaptee’s public interface does not leak into the adapter: callers see only play.

In real code, it is usually better to inject the adaptee than to create it inside the constructor. VLCAdapter(std::unique_ptr<VLCPlayer> p), as in the first snippet, lets tests pass a fake player and lets the application configure the real one (with a device, a log path, whatever it needs). The default-constructing version here is shorter but hard-wires the dependency. Note also that MediaPlayer has a virtual destructor in this version; the first snippet omitted it, and without it deleting an adapter through a unique_ptr<MediaPlayer> would skip the adapter’s destructor and leak its VLCPlayer.

class adapter

Class adapters are a way to use multiple inheritance. This is possible in C++, but is less flexible than an object adapter.

Multiple inheritance method

#include <iostream>
#include <string>
class MediaPlayer {
public:
    virtual void play(const std::string& filename) = 0;
    virtual ~MediaPlayer() = default;
};
class VLCPlayer {
public:
    void playVLC(const std::string& filename) {
        std::cout << "Playing VLC: " << filename << '\n';
    }
};
// class adapter (multiple inheritance)
class VLCAdapter : public MediaPlayer, private VLCPlayer {
public:
    void play(const std::string& filename) override {
playVLC(filename);  // call directly
    }
};
int main() {
    MediaPlayer* player = new VLCAdapter();
    player->play("movie.vlc");
    delete player;
}

Advantage: No need to store Adaptee object. Disadvantage: Multiple inheritance, not possible if Adaptee is final.

The private in private VLCPlayer matters. Public inheritance would make every VLCAdapter also usable as a VLCPlayer, exposing playVLC to callers and defeating the point of adapting. Private inheritance means “implemented in terms of”: the adapter can call the adaptee’s members and override its virtual functions, but outsiders only see the MediaPlayer interface.

A class adapter is occasionally the better tool. If the adaptee has protected members you need, or virtual functions you want to override to change its behaviour, only inheritance can reach them. It also avoids a separate allocation and a pointer indirection. The costs are that the adapter is tied to one concrete adaptee type at compile time, it cannot adapt an object that already exists, and multiple inheritance brings name-lookup and layout complications if the Target and Adaptee happen to share member names. The example also uses a raw new/delete pair for brevity; in real code, std::make_unique<VLCAdapter>() avoids leaking the object if play throws.


Legacy code integration

The Adapter Pattern shines when it comes to integrating legacy systems into a modern codebase. You can use the new interface without modifying existing code.

Old APIs into modern interfaces

#include <iostream>
#include <string>
#include <memory>
// Legacy API (C style)
class LegacyRectangle {
public:
    void draw(int x1, int y1, int x2, int y2) {
        std::cout << "Legacy: Rectangle from (" << x1 << "," << y1 
                  << ") to (" << x2 << "," << y2 << ")\n";
    }
};
// modern interface
class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() = default;
};
class Rectangle : public Shape {
public:
    Rectangle(int x, int y, int w, int h)
        : x_(x), y_(y), width_(w), height_(h) {}
    
    void draw() override {
        std::cout << "Modern: Rectangle at (" << x_ << "," << y_ 
                  << ") size " << width_ << "x" << height_ << '\n';
    }
    
private:
    int x_, y_, width_, height_;
};
// Adapter
class LegacyRectangleAdapter : public Shape {
public:
    LegacyRectangleAdapter(int x, int y, int w, int h)
        : x_(x), y_(y), width_(w), height_(h),
          legacy(std::make_unique<LegacyRectangle>()) {}
    
    void draw() override {
        legacy->draw(x_, y_, x_ + width_, y_ + height_);
    }
    
private:
    int x_, y_, width_, height_;
    std::unique_ptr<LegacyRectangle> legacy;
};
int main() {
    std::unique_ptr<Shape> shape1 = std::make_unique<Rectangle>(10, 20, 100, 50);
    shape1->draw();
    
    std::unique_ptr<Shape> shape2 = std::make_unique<LegacyRectangleAdapter>(10, 20, 100, 50);
    shape2->draw();
}

This example shows the most common kind of adaptation in practice: not a different method name, but a different data convention. The modern code thinks in (x, y, width, height), the legacy code in two corner points (x1, y1, x2, y2), and the adapter’s whole job is x2 = x + width. Conventions like this are where adapters earn their keep, and also where they hide bugs. Is the second corner inclusive or exclusive? Is y measured downwards (screen coordinates) or upwards (math coordinates)? Is the unit pixels, points or millimetres? An off-by-one between inclusive and exclusive corners produces rectangles that are one pixel too wide, which nobody notices until two shapes are supposed to touch.

When I write adapters around legacy APIs, the first thing I add is a small test that feeds known values through the adapter and checks exactly what the legacy call receives. Legacy APIs are usually poorly documented, and that test becomes the documentation of the conversion. It also catches the day someone “fixes” the legacy function and silently changes its convention.

Frequently occurring problems and solutions

Issue 1: Memory leak

Symptom: Memory leak. Cause: Use of raw pointer.

// ❌ Incorrect use
class Adapter {
Adaptee* adaptee;  // who delete?
};
// ✅ Correct use
class Adapter {
    std::unique_ptr<Adaptee> adaptee;
};

The raw pointer is not wrong in itself; the problem is that it does not say who owns the adaptee. If the adapter owns it, unique_ptr expresses that and frees it automatically. If someone else owns it and the adapter only uses it, a raw pointer or reference is fine, but then the adaptee must outlive the adapter, and that lifetime rule needs to be documented or enforced. Mixing the two, where some code paths delete adaptee and others do not, is how leaks and double frees appear.

Legacy C APIs make this sharper, because the adaptee is often a handle with its own release function (FILE* with fclose, a database connection with db_close). An adapter that owns such a handle should hold it in std::unique_ptr<FILE, decltype(&fclose)> or a small RAII wrapper and should delete its copy operations; otherwise copying the adapter copies the handle and both copies try to release it.

Issue 2: Bi-directional adapter

Symptom: Cyclodependency. Cause: Convert A to B and B to A.

// ✅ SOLVED: Common interface
class CommonInterface {
    virtual void operation() = 0;
};
class AdapterA : public CommonInterface { /* ... */ };
class AdapterB : public CommonInterface { /* ... */ };

A two-way adapter, one that lets A be used as B and B as A, sounds convenient but tends to create a dependency knot: the adapter header includes both libraries, both sides’ changes affect it, and it is easy to end up adapting A to B to A in a loop. Introducing an interface you own and adapting each external type to it separately keeps every adapter one-directional and independent. This is the same idea as “ports and adapters” (hexagonal architecture): your core code defines the ports (interfaces), and each outside system gets its own adapter to that port. The snippet omits public: before operation() and a virtual destructor for brevity; both are needed in real code.


production pattern

Pattern 1: Combined with Factory

class MediaPlayerFactory {
public:
    static std::unique_ptr<MediaPlayer> create(const std::string& type) {
        if (type == "vlc") {
            return std::make_unique<VLCAdapter>();
        } else if (type == "mp4") {
            return std::make_unique<MP4Adapter>();
        }
        return nullptr;
    }
};
auto player = MediaPlayerFactory::create("vlc");
player->play("movie.vlc");

The factory keeps the choice of concrete adapter in one place, which matters when that choice comes from configuration, file extensions or a plug-in list. It also means the rest of the program never names VLCAdapter directly. Be careful with the nullptr fallback: MediaPlayerFactory::create("avi")->play(...) dereferences a null pointer, which is undefined behavior and typically a segmentation fault far from the typo that caused it. Returning a “null player” that does nothing, throwing std::invalid_argument, or returning std::optional/an error type forces the caller to deal with unknown types.

Pattern 2: Template Adapter

template<typename Adaptee>
class GenericAdapter : public MediaPlayer {
public:
    GenericAdapter() : adaptee(std::make_unique<Adaptee>()) {}
    
    void play(const std::string& filename) override {
        adaptee->playSpecific(filename);
    }
    
private:
    std::unique_ptr<Adaptee> adaptee;
};

The template adapter only works if every adaptee has a member called playSpecific, which real third-party classes rarely share; if they did, you would barely need an adapter. It becomes useful in a slightly different form, where the template parameter is a small traits or policy type that knows how to call each adaptee, or where C++20 concepts constrain the adaptee (requires requires(Adaptee a, std::string s) { a.playSpecific(s); }) so that a mismatched type fails with a readable error instead of a long template instantiation trace. Without such a constraint, GenericAdapter<VLCPlayer> fails to compile with 'class VLCPlayer' has no member named 'playSpecific', pointing inside the template rather than at the line that instantiated it.


Complete example: payment system

#include <iostream>
#include <memory>
#include <string>
#include <cmath>
class PaymentProcessor {
public:
    virtual bool processPayment(double amount) = 0;
    virtual ~PaymentProcessor() = default;
};
// Legacy PayPal API
class PayPalAPI {
public:
    bool sendPayment(double dollars) {
        std::cout << "PayPal: Processing $" << dollars << '\n';
        return true;
    }
};
// Legacy Stripe API
class StripeAPI {
public:
    bool charge(int cents) {
        std::cout << "Stripe: Charging " << cents << " cents\n";
        return true;
    }
};
// New Square API
class SquareAPI {
public:
    bool makePayment(const std::string& amount) {
        std::cout << "Square: Payment of " << amount << '\n';
        return true;
    }
};
// Adapters
class PayPalAdapter : public PaymentProcessor {
public:
    PayPalAdapter() : paypal(std::make_unique<PayPalAPI>()) {}
    
    bool processPayment(double amount) override {
        return paypal->sendPayment(amount);
    }
    
private:
    std::unique_ptr<PayPalAPI> paypal;
};
class StripeAdapter : public PaymentProcessor {
public:
    StripeAdapter() : stripe(std::make_unique<StripeAPI>()) {}
    
    bool processPayment(double amount) override {
        // round, don't truncate: 0.29 * 100 is 28.999999..., which static_cast<int> turns into 28
        int cents = static_cast<int>(std::lround(amount * 100));
        return stripe->charge(cents);
    }
    
private:
    std::unique_ptr<StripeAPI> stripe;
};
class SquareAdapter : public PaymentProcessor {
public:
    SquareAdapter() : square(std::make_unique<SquareAPI>()) {}
    
    bool processPayment(double amount) override {
        return square->makePayment("$" + std::to_string(amount));
    }
    
private:
    std::unique_ptr<SquareAPI> square;
};
class PaymentService {
public:
    PaymentService(std::unique_ptr<PaymentProcessor> processor)
        : processor_(std::move(processor)) {}
    
    void checkout(double amount) {
        std::cout << "Processing checkout for $" << amount << '\n';
        if (processor_->processPayment(amount)) {
            std::cout << "Payment successful!\n\n";
        } else {
            std::cout << "Payment failed!\n\n";
        }
    }
    
private:
    std::unique_ptr<PaymentProcessor> processor_;
};
int main() {
    PaymentService service1(std::make_unique<PayPalAdapter>());
    service1.checkout(99.99);
    
    PaymentService service2(std::make_unique<StripeAdapter>());
    service2.checkout(49.50);
    
    PaymentService service3(std::make_unique<SquareAdapter>());
    service3.checkout(29.99);
}

The three vendors disagree on exactly the things adapters exist to smooth over: PayPal takes dollars as a double, Stripe takes integer cents, and Square takes a string. PaymentService knows none of that; it depends only on PaymentProcessor, receives its processor through the constructor, and could be tested with a fake processor that records the amount.

The unit conversion is where this example originally had a real bug, and it is the kind of bug that makes it to production. static_cast<int>(amount * 100) truncates, and because most decimal amounts are not exactly representable in binary floating point, 0.29 * 100 evaluates to 28.999999999999996 and becomes 28 cents. The fix above rounds with std::lround. The deeper fix is not to use double for money at all: represent amounts as integer minor units (cents) or a decimal type in your own interface, and convert to each vendor’s format only at the adapter boundary. The Square adapter has a similar formatting issue, since std::to_string(29.99) produces "29.990000", which a real API may reject; std::format("{:.2f}", amount) (C++20) or a stream with std::fixed << std::setprecision(2) gives the expected "29.99".

Real payment adapters also translate errors, not just arguments. Each vendor reports failure differently (HTTP status codes, exception types, error strings), and collapsing all of that to bool loses the information callers need to decide whether to retry, ask the user for a different card, or alert someone. A result type carrying a vendor-neutral error code, produced inside each adapter, keeps PaymentService independent of the vendors while still telling it what went wrong.

organize

conceptDescription
Adapter Patternconvert interface
PurposeIntegration of incompatible interfaces
StructureTarget, Adapter, Adaptee
AdvantagesLegacy Integration, OCP Compliance, Reusability
Disadvantagesclass increment, indirect reference
Use CaseLegacy integrations, third-party libraries, API conversions

Adapter Pattern is an essential pattern to unify incompatible interfaces.


FAQ

Q1: When do I use the Adapter Pattern?

A: Used to integrate legacy code, use third-party libraries, and resolve interface inconsistencies.

Q2: Object adapter vs class adapter?

A: Object adapter combination (recommended), Class adapter multiple inheritance (C++ possible).

Q3: What is the difference from Decorator?

A: Adapter focuses on interface conversion, Decorator focuses on feature addition.

Q4: What is the difference from Facade?

A: Adapter focuses on single class transformation, Facade focuses on subsystem simplification.

Q5: What is the performance overhead?

A: Usually one virtual call plus, for object adapters, one pointer indirection. That is negligible next to the work the adaptee does (playing media, making a network request). It only matters for adapters called millions of times per second on tiny operations, where a template-based adapter that the compiler can inline is the alternative.

Q6: What are the Adapter Pattern learning resources?

A:

  • “Design Patterns” by Gang of Four
  • “Head First Design Patterns” by Freeman & Freeman
  • Refactoring Guru: Adapter Pattern One-Line Summary: The Adapter Pattern allows you to integrate incompatible interfaces. Next, it would be a good idea to read the Bridge pattern, which separates abstraction from implementation up front instead of patching a mismatch afterwards.