Jonathan Boccara's blog

On Design Patterns in C++

Published December 18, 2020 - 0 Comments

Design patterns are a must-know in programming today.

The first reference to “design patterns” I know of is the famous GoF book:

design patterns book GoFThis book is a classic of programming and sits on the desk of many programmers across the world.

The Design patterns described in this book are various ways to structure code to solve specific problems. Those patterns have been labelled with names such as “Strategy”, “Visitor”, “Adapter”, “Chain of Responsibility” and so on.

Design patterns are not specific of a language: they can be applied in C++, Java, C#, and many other languages. But the code examples in the GoF book are in C++ and Smalltalk.

For a detailed analysis of those design patterns, refer to my series “Design Patterns VS Design Principles”. In this post I want to dig in a general aspect of design patterns in C++ that I didn’t find easy to see in the GoF book.

The seminal book on design patterns has C++ examples! Lucky us! To understand how to apply design patterns in C++, we can just have a look at the code snippets in the GoF book then, right?

Well, things are not so simple.

The C++ in the GoF book is not representative of C++

The features of C++ used in the GoF book do not take advantage of all the programming paradigms that C++ offers.

The main reason is that design patterns rely heavily on polymorphism, and the book exclusively uses runtime polymorphism in its examples, that is inheritance and virtual methods.

For example, here is an extract of the code illustrating the Visitor design pattern:

class EquipmentVisitor {
public:
    virtual ~EquipmentVisitor();
    virtual void VisitFloppyDisk(FloppyDisk*);
    virtual void VisitCard(Card*);
    virtual void VisitChassis(Chassis*);
    virtual void VisitBus(Bus*);
    // and so on for other concrete subclasses of Equipment
protected:
    EquipmentVisitor();
};

class PricingVisitor : public EquipmentVisitor {
public:
    PricingVisitor();
    Currency& GetTotalPrice();
    virtual void VisitFloppyDisk(FloppyDisk*);
    virtual void VisitCard(Card*);
    virtual void VisitChassis(Chassis*);
    virtual void VisitBus(Bus*);
    // ...
private:
     Currency _total;
};

This is one possible way of implementing Visitor in C++, and doesn’t use the specificities of the language. We’ll see another one below. In the above code, we could just rearrange the position of the keywords and transform this code into Java for example.

To be clear, my goal is not to pick on the GoF book. It is an excellent book, a very interesting read, and still relevant today despite the fact that it was written some 25 years ago. Both the design catalogue and the first part of the book (before the catalogue) contain valuable lessons of design.

But after reading the GoF book you may have the impression that this is the only way to implement design patterns in C++. And this is not true.

Modern C++ design

Another book on design patterns that was published after the GoF book is Andrei Alexandrescu’s Modern C++ Design:

Modern C++ DesignThe title of this book is not very specific but look at the subtitle: Generic Programming and Design Patterns Applied. Generic programming means templates. This book is about implementing design patterns with templates.

In fact there is more to this book than that. It shows advanced templates and design techniques. And it uses those techniques to implement design patterns in ways that are very specific to C++.

This is my all-time favourite C++ book, and I recommend that you have a look at it. The templates have aged a little since it was published in 2001, long before variadic templates entered the standard, but it emulates of variadic templates with typelists.

It would be awesome to have a revised edition of this book with features of C++20. Still, the contents of this book remain interesting–and impressive!–today.

We won’t get into the advanced techniques Modern C++ Design here. Instead we’re going to see two simple examples in the C++ standard library that use design patterns with other tools than inheritance and virtual methods.

Strategy with std::for_each

Consider the simplest algorithm of the STL algorithms library: std::for_each:

template<class InputIterator, class UnaryFunction>
UnaryFunction for_each(InputIterator first, InputIterator last, UnaryFunction f);

std::for_each iterates over a range and applies a function to it:

auto numbers = std::vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
std::for_each(begin(numbers), end(numbers), [](int& i){ i *= 10; });

// number now contains { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 }

std::for_each implement the design pattern Strategy. Indeed, it takes a polymorphic parameter and uses it in its code. We could pass in any function or function to for_each as long as it accepts the type of parameters in the range denoted by the iterators.

Contrary to inheritance and virtual methods, this not runtime polymorphism. This is compile time polymorphism, based on templates, and that is resolved at compile time.

To be fair with the GoF book, even though it doesn’t mention std::for_each, one of the implementation remarks it mentions is that C++ allows to implement Strategy with a template parameter.

Visitor with std::visit

Let’s take another example with the Visitor design pattern. Visitor is essentially when you have a polymorphic function to apply on a polymorphic object.

The GoF book illustrates this with two hierarchies of classes, and shows how they can invoke virtual methods from one another to successively resolve the two instances of polymorphism.

To be fair again with the excellent GoF book, it mentions that this is a way to compensate for the double dispatch technique that C++ doesn’t implement (it mentions that the CLOS language implements it), which suggests that the double hierarchy of class is not the only way of implementing the pattern.

C++17’s std::visit gives another example of implementing the Visitor design pattern. std::visit is a way to apply a function on a std::variant. But since, by definition, a std::variant can hold values of different type, we may need various functions to operate on it.

Consider the following example. This is one of the various techniques to create a function object that can operate on various types in C++:

struct Visitor
{
    std::string operator()(std::string const& s){ return s; }
    
    template<typename T>
    std::string operator()(T const& value) { return std::to_string(value);}
};

Let’s now assume that we have a function that allows to get a variant object:

std::variant<int, std::string, char> getNumber();

Then we can apply the visitor on the variant object with std::visit:

std::string s = std::visit(Visitor{}, getNumber());

Here the visited object (the variant) uses runtime polymorphism (even though without inheritance and virtual methods), and the visitor object (the Visitor) uses compile time polymorphism based on overload resolution.

Various types of polymorphism

Even if the GoF books hints that there are other possible implementations, its code examples make heavy usage of inheritance and virtual methods. This is one way of implementing polymorphism that has its advantages and drawbacks.

But keep in mind that C++ is a rich language that offers various types of polymorphism, including compile time polymorphism that offers a different trade-off.

Be aware of the many tools at your disposal to be able to use the most adapted one in the situations you encounter.

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin