13.1. The Adapter pattern

An adapter converts one interface into another interface that clients expect. It lets existing code work with a client without changing either the client or the adapted component.

For example, imagine an existing temperature sensor that reports Celsius:

class celsius_sensor{
public:
  constexpr explicit celsius_sensor(double value)
    :value_{value}
  {}

  constexpr double read_celsius() const {
    return value_;
  }

private:
  double value_;
};

Suppose a client expects a sensor with a read_fahrenheit() operation instead. The existing sensor and the client have compatible responsibilities, but incompatible interfaces. An adapter supplies the missing conversion:

        classDiagram
   Client --> FahrenheitAdapter : uses
   FahrenheitAdapter --> CelsiusSensor : reads
   class FahrenheitAdapter {
      +read_fahrenheit() double
   }
   class CelsiusSensor {
      +read_celsius() double
   }
    

13.1.1. Traditional adapter implementations

The traditional design-pattern descriptions distinguish class adapters from object adapters.

A class adapter uses inheritance. The adapter derives from the adapted class and adds the interface that clients expect:

class fahrenheit_class_adapter : private celsius_sensor{
public:
  using celsius_sensor::celsius_sensor;

  constexpr double read_fahrenheit() const {
    return read_celsius() * 9.0 / 5.0 + 32.0;
  }
};

This approach is simple, but it couples the adapter to one concrete base class. The private inheritance keeps read_celsius() out of the adapter's public interface. Clients see the Fahrenheit operation, while the adapter can still call the inherited Celsius operation internally.

An object adapter uses composition. The adapter stores a reference or object of the adapted type and delegates through that member:

class fahrenheit_object_adapter{
public:
  constexpr explicit fahrenheit_object_adapter(
      const celsius_sensor& sensor)
    :sensor_{sensor}
  {}

  constexpr double read_fahrenheit() const {
    return sensor_.read_celsius() * 9.0 / 5.0 + 32.0;
  }

private:
  const celsius_sensor& sensor_;
};

Composition avoids an inheritance relationship and lets one adapter wrap an existing object. The reference also makes the ownership rule explicit: the sensor must outlive the adapter.

13.1.2. C++ adapter implementations

C++ does not require a class to inherit from an interface. A client can use any object that supplies the operations it needs. Templates make this flexibility useful: an adapter can be parameterized by the adapted type instead of being tied to one concrete class.

Since C++20, a concept can document and check the interface required by the adapter:

template <class Sensor>
concept celsius_readable = requires(const Sensor& sensor){
  { sensor.read_celsius() } -> std::convertible_to<double>;
};

The concept does not create a base class or add run-time overhead. It states the expressions that a type must support. The adapter can then use any matching sensor type:

template <celsius_readable Sensor>
class fahrenheit_adapter{
public:
  constexpr explicit fahrenheit_adapter(const Sensor& sensor)
    :sensor_{sensor}
  {}

  constexpr double read_fahrenheit() const {
    return sensor_.read_celsius() * 9.0 / 5.0 + 32.0;
  }

private:
  const Sensor& sensor_;
};

This adapter is still an object adapter because it uses composition, but its interface is checked at compile time and it can wrap any suitable sensor. There is no virtual function, base class, or run-time type check.

The complete example is small enough to compile and run:

 1#include <concepts>
 2#include <iostream>
 3
 4class celsius_sensor{
 5public:
 6  constexpr explicit celsius_sensor(double value)
 7    :value_{value}
 8  {}
 9
10  constexpr double read_celsius() const {
11    return value_;
12  }
13
14private:
15  double value_;
16};
17
18template <class Sensor>
19concept celsius_readable = requires(const Sensor& sensor){
20  { sensor.read_celsius() } -> std::convertible_to<double>;
21};
22
23template <celsius_readable Sensor>
24class fahrenheit_adapter{
25public:
26  constexpr explicit fahrenheit_adapter(const Sensor& sensor)
27    :sensor_{sensor}
28  {}
29
30  constexpr double read_fahrenheit() const {
31    return sensor_.read_celsius() * 9.0 / 5.0 + 32.0;
32  }
33
34private:
35  const Sensor& sensor_;
36};
37
38int main(){
39  celsius_sensor sensor{20.0};
40  fahrenheit_adapter adapter{sensor};
41  std::cout << "20C == 68F: " << std::boolalpha
42            <<  (adapter.read_fahrenheit() == 68.0);
43}

The standard library also uses the adapter pattern for its container adaptors. std::stack, std::queue, and std::priority_queue expose purpose-specific interfaces over underlying containers. The next pages use std::stack and std::queue as concrete container-adaptor examples; std::priority_queue is covered later.