🔴 🟡 🟢Design Patterns in Modern C++20
← Back to Posts

Design Patterns in Modern C++20

These are my notes while going through the book Design Patterns in Modern C++20: Reusable Approaches for Object-Oriented Software Design by Dmitri Nesteruk. Since they're notes, I may have direct code examples or quotes, and may omit things that may be notable to you but aren't to me. I definitely encourage getting a copy of this book for full context!

CRTP (Curiously Recurring Template Pattern)

Key idea: inheritor passes itself as a template argument to its base class. One reason to do this: access a typed this pointer in a base class implementation: Let's say every child class of SomeBase has a begin()/end() defined for iteration. Can you iterate the object in a member of SomeBase instead of in the child class? SomeBase doesn't itself provide a begin()/end(), so you may think no! but using CRTP, derived class can pass information about itself to the base class:

struct MyClass : SomeBase<MyClass>
{
  class iterator {
	// your iterator defined here
  }
  iterator begin() const { ... }
  iterator end() const { ... }
}

so inside the base class you can cast this to a derived class type:

template <typename Derived>
struct SomeBase
{
  void foo()
  {
	for (auto& item : *static_cast<Derived*>(this))
	{
	  ...
	}
  }
}

What happens here when calling MyClass::foo(): this pointer gets cast from SomeBase* to MyClass* , the pointer gets dereferenced and iterated on using that range-based for which calls the MyClass::begin()/end().

Mixin Inheritance

Key idea: a class can be defined to inherit from its own template argument, allowing hierarchical composition of types.

template <typename T> struct Mixin : T
{
  ...
}

You can make an instance of Foo<Bar<Baz>> x; that implements traits of all three classes without having to construct a whole new FooBarBaz type. This is really useful with Concepts! We can clearly and elegantly constrain the type our mixin inherits from to use base type features without having to look at compile-time errors.

Static Polymorphism

First, the old-fashioned way:

Ex: building an alert systems that notifies via different mediums: email, SMS, Telegram, etc. With CRTP you can implement a base Notifier :

template <typename TImpl>
class Notifier {
public:
  void AlertSMS(string_view msg)
  {
    impl().SendAlertSMS(msg);
  }
  void AlertEmail(string_view msg)
  {
    impl().SendAlertEmail(msg);
  }
private:
  TImpl& impl() { return static_cast<TImpl&>(*this); }
  friend TImpl;
};

TImpl is a template argument, so compiler will check that AlertSMS() and AlertEmail() actually exist on the object, even though we didn't tell it that TImpl must inherit from Notifier. So we can define this:

template <typename TImpl>
void AlertAllChannels(Notifier<TImpl>& notifier, string_view msg)
{
  notifier.AlertEmail(msg);
  notifier.AlertSMS(msg);
}

And then we can make Notifier implementations, like a no-op for testing:

struct TestNotifier: public Notifier<TestNotifier>
{
  void SendAlertSMS(string_view msg){}
  void SendAlertEmail(string_view msg){}
};

TestNotifier tn;
AlertAllChannels(tn, "testing!"); // just testing!

But this can be annoying because

  • you have to have two separate, parallel APIs e.g. AlertSMS/SendAlertSMS
  • the impl thing feels clunky. You'd expect base alert methods to be virtual and then impl class to override.
  • Timpl's interface isn't explicitly enforced, we just try to call them to check at runtime and rely on compiler errors.

Now, with Concepts:

we can alleviate those issues by introducing a concept that requires those member functions:

template <typename TImpl>
concept IsANotifier = requires(TImpl impl) {
  impl.AlertSMS(string_view{});
  impl.AlertEmail(string_view{});
};

So we don't need the base Notifier:

template <IsANotifier TImpl>
void AlertAllChannels(TImpl& impl, string_view msg)
{
  impl.AlertSMS(msg);
  impl.AlertEmail(msg);
}

struct TestNotifier
{
  void AlertSMS(string_view msg) {}
  void AlertEmail(string_view msg) {}
};

Properties

A property is just a combination of a (usually private) field and a getter/setter. This isn't part of the C++ standard (and you could implement this without any special language support), but we should mention the declaration specifier property you can use in most compilers:

class Person
{
  int age_;
public:
  int get_age() const { return age_; }
  void set_age(int value) { age_ = value; }
  __declspec(property(get=get_age, put=set_age)) int age;
};

Here __declspec(property()) turns the getters and setters into virtual fields, so the compiler replaces any attempts to read/write to this field.

Person p;
p.age = 20; // calls p.set_age(20)

SOLID Design Principles

Single Responsibility Principle (SRP)

"Each class should have only one responsibility and therefore one reason to change."

Ex: A Journal class shouldn't have to deal with persistence/file saving, it should only need to change if e.g. you need to change how journal entries are stored. (Any situation that leads us to having to do a lot of tiny changes in lots of classes is probably a code smell!)

An extreme example of an antipattern here is the God object -- a huge class that tries to handle as many concerns as possible -- terrible monolithic monstrosity!

Open-Closed Principle (OCP)

"open for extension but closed for modification" -- You shouldn't need to go back to code you've written and tested to change it.

Ex: You have a bunch of products, and you want to be able to filter them in many ways: by color, by size, by color and size...

enum class Color { Red, Green, Blue };
enum class Size { Small, Medium, Large };
struct Product
{
  string name;
  Color color;
  Size size;
};

Let's first separate (SRP) the filter (process that takes all items and only returns some) and the specification (the predicate definition to apply to a data element).

template <typename T> struct Specification
{
  virtual bool is_satisfied(T* item) = 0;
};

template <typename T> struct Filter
{
  virtual vector<T*> filter(
    vector<T*> items,
    Specification<T>& spec) const = 0;
};

Then the filter implementation is way simpler:

struct BetterFilter : Filter<Product>
{
  vector<Product*> filter(
    vector<Product*> items,
    Specification<Product>& spec) override
  {
    vector<Product*> result;
    for (auto& p : items)
      if (spec.is_satisfied(p))
        result.push_back(p);
    return result;
  }
};

And for actual specialized filters:

struct ColorSpecification : Specification<Product>
{
  Color color;
  explicit ColorSpecification(const Color color) : color{color} {}
  bool is_satisfied(Product* item) override {
    return item->color == color;
  }
};

Product apple{ "Apple", Color::Green, Size::Small };
Product tree{ "Tree", Color::Green, Size::Large };
Product house{ "House", Color::Blue, Size::Large };
vector<Product*> all{ &apple, &tree, &house };
BetterFilter bf;
ColorSpecification green(Color::Green);
auto green_things = bf.filter(all, green);
for (auto& x : green_things)
  cout << x->name << " is green";

And for compositional conditions:

template <typename T> struct AndSpecification : Specification<T>
{
  Specification<T>& first;
  Specification<T>& second;
  AndSpecification(Specification<T>& first,
    Specification<T>& second)
    : first{first}, second{second} {}
  bool is_satisfied(T* item) override
  {
    return first.is_satisfied(item) && second.is_satisfied(item);
  }
};

SizeSpecification large(Size::Large);
ColorSpecification green(Color::Green);
AndSpecification<Product> green_and_large{ large, green };
auto big_green_things = bf.filter(all, green_and_big);
for (auto& x : big_green_things)
  cout << x->name << " is large and green";
// Tree is large and green

Side note: C++26 reflection + concepts can further make this way less clunky!

You can also embellish:

template <typename T> AndSpecification<T> operator&&
  (const Specification<T>& first,
   const Specification<T>& second)
{
  return { first, second };
}

SizeSpecification large(Size::Large);
ColorSpecification green(Color::Green);
auto big_green_things = bf.filter(all, green && large);
for (auto& x : big_green_things)
  cout << x->name << " is large and green" << endl;

Liskov Substitution Principle (LSP)

"If an interface takes an object of type Parent, it should equally take an object of type Child without anything breaking."

Bad example:

class Rectangle
{
protected:
  int width, height;
public:
  Rectangle(const int width, const int height)
    : width{width}, height{height} { }
  int get_width() const { return width; }
  virtual void set_width(const int width) { this->width = width; }
  int get_height() const { return height; }
  virtual void set_height(const int height) { this->height = height; }
  int area() const { return width * height; }
};

class Square : public Rectangle
{
public:
  Square(int size): Rectangle(size,size) {}
  void set_width(const int width) override {
    this->width = height = width;
  }
  void set_height(const int height) override {
    this->height = width = height;
  }
};

Seems innocent, but then this easily blows up:

void process(Rectangle& r)
{
  int w = r.get_width();
  r.set_height(10);
  cout << "expected area = " << (w * 10)
    << ", got " << r.area() << endl;
}

Square s{5};
process(s); // expected area = 50, got 25

Ahh!!! Many possible solutions: perhaps the type Square shouldn't even exist and we should maybe use Factories for both rectangles and squares. Or maybe detect that a Rectangle is in fact a Square. You could also throw an exception but this would violate the "principle of least surprise" -- you'd probably expect a call to set_width to actually do something?

Interface Segregation Principle (ISP)

"Split up interfaces so that implementers can pick and choose depending on their needs."

Ex: instead of having an interface for your multifunction printer that prints, scans, and faxes, you should have 3 separate interfaces for each.

Dependency Inversion Principle (DIP)

"Depending on interfaces or base classes is better than depending on concrete types." "Abstractions should not depend on details. Details should depend on abstractions."

We can achieve this via dependency injection:

Ex. Car has an engine, but needs to write to a log.

struct Engine
{
  float volume = 5;
  int horse_power = 400;
  friend ostream& operator<< (ostream& os, const Engine& obj)
  {
    return os
      << "volume: " << obj.volume
      << " horse_power: " << obj.horse_power;
  } // thanks, ReSharper!
};

struct ILogger
{
  virtual ~ILogger() {}
  virtual void Log(const string& s) = 0;
};

struct ConsoleLogger : ILogger
{
  ConsoleLogger() {}
  void Log(const string& s) override
  {
    cout << "LOG: " << s.c_str() << endl;
  }
};

Car needs both the engine and logging component. Let's define both of the dependent components as constructor parameters:

struct Car
{
  unique_ptr<Engine> engine;
  shared_ptr<ILogger> logger;
  Car(unique_ptr<Engine> engine,
      const shared_ptr<ILogger>& logger)
    : engine{move(engine)},
      logger{logger}
  {
    logger->Log("making a car");
  }
  friend ostream& operator<<(ostream& os, const Car& obj)
  {
    return os << "car with engine: " << *obj.engine;
  }
};

Then we bind ILogger to ConsoleLogger: "any time someone asks for an ILogger give them a ConsoleLogger"

auto injector = di::make_injector(
  di::bind<ILogger>().to<ConsoleLogger>()
);

And now we can create our car:

auto car = injector.create<shared_ptr<Car>>();

So we have a shared_ptr<Car> pointing to a fully initialized Car, and to change the type of logger we just have to change a single place (the bind call) and then everywhere there's an ILogger it can now be the new logger. Helps with unit testing and allows us to use stubs/noops instead of mocks!