Hello! Welcome to your next lesson on C++ design patterns.
In our last session, we wrapped up creational patterns by mastering the Builder. We saw how it provides a flexible and readable way to construct complex objects step-by-step, contrasting the classic "Gang of Four" implementation with modern, fluent C++ idioms using features like CTAD and CRTP.
Today, we pivot from creational patterns to structural patterns. These patterns are concerned with how classes and objects are composed to form larger structures. Our first stop is the Adapter pattern, a workhorse pattern you'll encounter frequently when integrating different systems. Its purpose is elegantly simple: to make incompatible interfaces work together.
Your learning outcome for this lesson is to implement the Adapter pattern to make an existing class's interface compatible with another. We will analyze the problem it solves, dissect its two primary implementation strategies—Object and Class Adapters—and explore the C++-specific mechanics that make them work.
1. The Problem: Incompatible Interfaces
Imagine you are working on a new application that requires a logging mechanism. The application's framework expects all loggers to conform to a specific Logger interface, perhaps with a method like log_message(level, message). However, your team has a subscription to a powerful, highly-optimized, third-party logging library. The only problem? Its main class, SuperLogger, has a completely different method signature, like writeToLog(const char* data, int severity).
You are faced with a classic integration problem:
- You cannot change the application framework's
Loggerinterface. - You cannot (or should not) modify the source code of the
SuperLoggerlibrary.
How do you bridge this gap? This is precisely the problem the Adapter pattern solves. It acts like a real-world power adapter, allowing you to plug a device with one type of plug into an outlet of another type.
2. The Core Concept and Participants
The Adapter pattern works by creating a "wrapper" object that has the interface the client expects and translates the client's calls into calls on the incompatible object (the "adaptee").
The pattern has four key participants:
- Target: The interface that the client code uses and expects. In our example, this is the
Loggerclass with thelog_message()method. - Client: The code that interacts with an object that conforms to the
Targetinterface. - Adaptee: The existing class with an incompatible interface that we want to use. In our example, this is the
SuperLoggerwith itswriteToLog()method. - Adapter: The class that "wraps" the
Adapteeand implements theTargetinterface. It translates calls from theTargetinterface into calls to theAdaptee's interface.
This structure allows the Client to use the Adaptee's functionality without ever knowing about its incompatible interface.

To get a formal understanding of the pattern's intent and structure, let's start with a foundational reading.
This document, 'Adapter Design Pattern', provides a concise introduction to the pattern's intent and the two primary implementation strategies. It clearly defines the roles of the participants.
Please read the 'Introduction' (Section 19.1) and 'Adapter Design Pattern' (Section 19.2) up to, but not including, Section 19.3. Focus on the 'Intent' and the description of the 'Participants'.
3. Implementation Strategies in C++
C++ provides two main ways to implement the Adapter pattern, each with different trade-offs that are important to understand.
Strategy 1: The Object Adapter (using Composition)
This is the more common and flexible approach. The Adapter class implements the Target interface and holds an instance of the Adaptee as a member variable. When a client calls a method on the Adapter, the adapter forwards the call to its internal Adaptee instance, translating arguments as needed.
This approach follows the well-known principle: "Prefer composition over inheritance."
Let's see this in action with a concrete example from the resource you just reviewed. Imagine we have an ElectronicBillboard (the Adaptee) with a complex interface, and we want to provide a simpler Billboard interface (the Target).
// Adaptee: The existing complex class
class ElectronicBillboard {
public:
ElectronicBillboard(const std::string& msg) { /*...*/ }
void updateMessage(const std::string& msg) { /*...*/ }
void onoff() { /*...*/ }
bool ison() const { /*...*/ }
void updateBoard() { /*...*/ }
// ... other methods
};
// Target: The interface our client expects
class Billboard {
public:
virtual ~Billboard() = default;
virtual void changeState() = 0;
virtual void changeMessage(int msgId) = 0;
virtual void displayMessage() = 0;
};
// Object Adapter: Implements Target, contains Adaptee
class TrafficBillboardObjectAdapter : public Billboard {
public:
TrafficBillboardObjectAdapter() {
// The Adapter owns the Adaptee instance
board = new ElectronicBillboard("all clear");
}
// In a real implementation, we would use std::unique_ptr
~TrafficBillboardObjectAdapter() {
delete board;
}
void changeState() override {
board->onoff(); // Delegate the call
}
void changeMessage(int msgId) override {
// Translate from msgId to a string message
std::string msg;
switch (msgId) {
case 1: msg = "slow traffic ahead"; break;
case 2: msg = "accident ahead"; break;
default: msg = "all clear";
}
board->updateMessage(msg); // Delegate the call
}
void displayMessage() override {
if (board->ison()) {
board->updateBoard(); // Delegate the call
}
}
private:
ElectronicBillboard* board; // Composition: Adapter "has-a" Adaptee
};
This code is based on "Listing 1" in the Adapter Design Pattern PDF. Notice how TrafficBillboardObjectAdapter inherits from Billboard (the Target) and contains a pointer to an ElectronicBillboard (the Adaptee). It fulfills the Billboard contract by translating calls and delegating them to the board object.
Strategy 2: The Class Adapter (using Private Inheritance)
This strategy uses multiple inheritance. The Adapter inherits publicly from the Target interface and privately from the Adaptee implementation.
This is a very C++-specific approach that leverages an often-misunderstood feature: private inheritance. Public inheritance implies an "is-a" relationship (a Car is a Vehicle). Private inheritance implies an "is-implemented-in-terms-of" relationship. The public interface of the base class (Adaptee) is hidden from clients of the Adapter, but its implementation (including public and protected members) is available internally to the Adapter.
For someone with your interest in C++ internals, understanding this distinction is crucial.
To properly understand the Class Adapter, we first need to review how private and protected inheritance work in C++. This section of the PDF provides a perfect, concise explanation.
Read Section 19.3, 'Protected and private inheritance in C++ explained'. Pay close attention to the table showing how base class member visibility changes. Note the key takeaway that private inheritance is a form of a 'has-a' relationship.
Now, let's look at the Class Adapter version of our TrafficBillboard.
// Target and Adaptee are the same as before.
// Class Adapter: Inherits from Target and Adaptee
class TrafficBillboardClassAdapter : public Billboard, private ElectronicBillboard {
public:
TrafficBillboardClassAdapter() : ElectronicBillboard("all clear") {}
void changeState() override {
onoff(); // Call the base class (Adaptee) method directly
}
void changeMessage(int msgId) override {
std::string msg;
switch (msgId) {
case 1: msg = "slow traffic ahead"; break;
case 2: msg = "accident ahead"; break;
default: msg = "all clear";
}
updateMessage(msg); // Call the base class method
}
void displayMessage() override {
if (ison()) {
updateBoard(); // Call the base class method
}
}
// No member variable needed, the implementation is inherited.
};
This is based on "Listing 2" in the PDF. Notice the key differences:
- The class declaration is
class ... : public Billboard, private ElectronicBillboard. - There's no
ElectronicBillboard* board;member. TheAdapteris anElectronicBillboardin terms of implementation. - Methods like
onoff()andupdateMessage()are called directly because they are part of theAdapter's inherited implementation fromElectronicBillboard.
Comparing the Strategies
| Feature | Object Adapter (Composition) | Class Adapter (Inheritance) |
|---|---|---|
| Flexibility | High. A single adapter can work with the Adaptee and any of its subclasses. |
Low. It's tied to one specific Adaptee class. |
| Implementation | Requires instantiating an Adaptee object. |
Doesn't require a separate object. Uses C++ multiple inheritance. |
| Overriding Adaptee | Cannot directly override Adaptee's behavior. |
Can override Adaptee's virtual methods (if any). |
| Recommendation | Generally preferred. More flexible and aligns with "prefer composition over inheritance". | Useful in specific cases, but can be less flexible. Requires careful use of private inheritance. |
Test your understanding!
You are tasked with integrating a legacy FileLogger class that writes messages to a file. Its interface is void write(const std::string& line). Your application uses a Sink interface with the method void submit(const LogEntry& entry), where LogEntry is a struct containing a timestamp, severity level, and message.
Which adapter strategy (Object or Class) would you choose, and why? Briefly sketch out the adapter class's declaration.
Show answer
The Object Adapter strategy is the better choice here. It is more flexible and avoids the complexities of multiple inheritance, especially when dealing with potentially complex legacy classes. It clearly separates the interface (Sink) from the implementation (FileLogger).
Here is a sketch of the class declaration:
#include <string>
#include <memory> // For std::unique_ptr
// Adaptee (Legacy Class)
class FileLogger {
public:
void write(const std::string& line) { /* ... writes to file ... */ }
};
// Target (Application Interface)
struct LogEntry { /* ... */ };
class Sink {
public:
virtual ~Sink() = default;
virtual void submit(const LogEntry& entry) = 0;
};
// The Object Adapter
class FileLoggerAdapter : public Sink {
public:
FileLoggerAdapter() : adaptee_(std::make_unique<FileLogger>()) {}
void submit(const LogEntry& entry) override {
// 1. Translate LogEntry to a string
std::string line = format_entry(entry);
// 2. Delegate the call to the adaptee
adaptee_->write(line);
}
private:
std::unique_ptr<FileLogger> adaptee_; // Composition
std::string format_entry(const LogEntry& entry) const {
// ... logic to convert LogEntry to a single string ...
return "Formatted line";
}
};
This design is clean, safe (using std::unique_ptr for ownership), and easy to understand.
4. Advanced Topic: Adapter vs. Related Patterns
The Adapter pattern's intent—changing an interface—can seem similar to other patterns. Distinguishing them is key to using them correctly. The Decorator pattern, for example, also wraps an object, but its intent is to add responsibilities without changing the interface.
A more subtle and advanced comparison can be made with the External Polymorphism pattern. This is an interesting case for you, as it allows classes without virtual functions (and thus no vptr and associated overhead) to be treated polymorphically. It uses techniques from the Adapter pattern to achieve this.
Let's examine how the authors of this advanced pattern distinguish it from the standard Adapter pattern.
This paper on 'External Polymorphism' provides a fantastic, nuanced comparison to the Adapter pattern. The goal here isn't to learn External Polymorphism fully, but to use its analysis to sharpen your understanding of what makes the Adapter pattern unique.
Read Section 1.10, 'Related Patterns'. Focus on the four points that distinguish External Polymorphism from the GoF Adapter pattern: Intents differ, Layer vs. Peer, Extension vs. Conversion, and Behavior vs. Interface.
The key distinction highlighted in the paper is that an Adapter's primary intent is conversion for a specific client, while a pattern like External Polymorphism's intent is to create a new way to access similar functionality across unrelated types. Adapter is about making one specific thing fit. Other, more advanced patterns might use similar wrapping techniques to build entirely new systems of behavior.
Conclusion
Today we have explored the Adapter pattern, a fundamental structural tool for making disparate parts of a system work together. It's a pragmatic solution to the everyday engineering problem of integration.
Key Takeaways:
- The Adapter pattern converts the interface of a class into another interface that a client expects.
- It's a structural pattern used to bridge incompatibility between classes, often when integrating legacy or third-party code.
- The Object Adapter uses composition, is more flexible, and is generally the preferred implementation.
- The Class Adapter uses private inheritance in C++ to achieve an "is-implemented-in-terms-of" relationship. This is less flexible but can be suitable in some cases.
- The Adapter's intent of interface conversion distinguishes it from other wrapper-based patterns like Decorator, which adds functionality.
In our next lesson, we will dive into the Decorator pattern. You'll see how it also uses object wrapping, but for a very different purpose: to attach additional responsibilities to an object dynamically and transparently. This comparison will further solidify your understanding of structural patterns.