Visitor, Strategy, Decorator. These are all names of design patterns that we’ll deal with in the upcoming chapters. However, before taking a detailed look at each of these design patterns, I should give you an idea about the general purpose of a design pattern. Thus in this chapter, we will first take a look at the fundamental properties of design patterns, why you would want to know about them and use them.
In “Guideline 1: Understand the Importance of Software Design”, I already used the term design pattern and explained on which level of software development you use them. However, I have not yet explained in detail what a design pattern is. That will be the topic of “Guideline 11: Understand the Purpose of Design Patterns”: you will understand that a design pattern has a name that expresses an intent, introduces an abstraction that helps to decouple software entities, and has been proven over the years.
In “Guideline 12: Beware of Design Pattern Misconceptions”, I will focus on several misconceptions about design patterns and explain what a design pattern is not. I will try to convince you that design patterns are not about implementation details and do not represent language-specific solutions to common problems. I will also do my best to show you that they are not limited to object-oriented programming nor to dynamic polymorphism.
In “Guideline 13: Design Patterns Are Everywhere”, I will demonstrate that it’s hard to avoid design patterns. They are everywhere! You will realize that the C++ Standard Library in particular is full of design patterns and makes good use of their strengths.
In “Guideline 14: Use a Design Pattern’s Name to Communicate Intent”, I will make the point that part of the strength of a design pattern is the ability to communicate intent by using its name. Thus I will show you how much more information and meaning you can add to your code by using the name of a design pattern.
There’s a good chance that you have heard about design patterns before and a fairly good chance that you’ve used some of them in your programming career. Design patterns are nothing new: they have been around at least since the Gang of Four (GoF) released their book on design patterns in 1994.1 And while there are always critics, their special value has been acknowledged throughout the software industry. Yet, despite the long existence and importance of design patterns, despite all the knowledge and accumulated wisdom, there are many misconceptions about them, especially in the C++ community.
To use design patterns productively, as a first step you need to understand what design patterns are. A design pattern:
Has a name
Carries an intent
Introduces an abstraction
Has been proven
First of all, a design pattern has a name. While this sounds very obvious and necessary, it is indeed a fundamental property of a design pattern. Let’s assume that the two of us are working on a project together and are tasked with finding a solution to a problem. Imagine I told you, “I would use a Visitor for that.”2 Not only would this tell you what I understand to be the real problem, but it would also give you a precise idea about the kind of solution I’m proposing.
The name of a design pattern allows us to communicate on a very high level and to exchange a lot of information with very few words:
ME: I would use a Visitor for that.
YOU: I don’t know. I thought of using a Strategy.
ME: Yes, you may have a point there. But since we’ll have to extend operations fairly often, we probably should consider a Decorator as well.
By just using the names Visitor, Strategy, and Decorator, we’ve discussed the evolution of the codebase, and described how we expect things to change and to be extended in years to come.3 Without these names, we would have a much harder time expressing our ideas:
ME: I think we should create a system that allows us to extend the operations without the need to modify existing types again and again.
YOU: I don’t know. Rather than new operations, I would expect new types to be added frequently. So I prefer a solution that allows me to add types easily. But to reduce coupling to the implementation details, which is to be expected, I would suggest a way to extract implementation details from existing types by introducing a variation point.
ME: Yes, you may have a point there. But since we’ll have to extend operations fairly often, we probably should consider designing the system in such a way that we can build on and reuse a given implementation easily.
Do you see the difference? Do you feel the difference? Without names, we have to talk about a lot more details explicitly. Obviously this kind of precise communication is possible only if we share the same understanding of design patterns. That is why it’s so important to know about design patterns and to talk about them.
By using the name of a design pattern, you can express your intent concisely and limit possible misunderstandings. This leads to the second property of a design pattern: an intent. The name of a design pattern conveys its intent. If you use the name of a design pattern, you implicitly state what you consider to be the problem and what you see as a solution.
Hopefully you realized that in our little conversion, we weren’t talking about any kind of implementation. We didn’t talk about implementation details, any features, or any particular C++ standard. We didn’t even talk about any particular programming language. And please don’t assume that by giving you the name of a design pattern I have implicitly told you how to implement the solution. That is not what a design pattern is about. On the contrary: the name should tell you about the structure that I propose, about how I plan to manage dependencies and about how I expect the system to evolve. That is the intent.
In fact, many design patterns have a similar structure. In the GoF book, many of the design patterns look very much alike, which, of course, raises a lot of confusion and questions. For instance, structurally, there appears to be almost no difference between the Strategy, the Command, and the Bridge design patterns.4 However, their intent is very different and you would therefore use them to solve different problems. As you will see in various examples in the following chapters, there are almost always many different implementations you can choose from.
A design pattern always provides some way to reduce dependencies by introducing some
kind of abstraction. This means that a design pattern is always concerned with managing
the interaction between software entities and decoupling pieces of your software. For example, consider the Strategy design pattern, one of the original GoF design
patterns, in Figure 3-1. Without going into too much detail, the Strategy
design pattern introduces an abstraction in the
form of the Strategy
base class. This base class decouples the Strategy
user (the
Context
class in the high level of your architecture) from the implementation details
of the concrete strategies (ConcreteStrategyA
and ConcreteStrategyB
in the low level
of your architecture). As such, Strategy fulfills the properties of a design
pattern.5
A similar example is the Factory Method design pattern (yet another GoF design pattern;
see Figure 3-2). The intent of Factory Method is to decouple from the
creation of specific products. For that purpose, it introduces two abstractions in the form
of the Product
and Creator
base classes, which architecturally reside in the high level.
The implementation details, given by means of the ConcreteProduct
and ConcreteCreator
classes, reside on the low level of the architecture. With this architectural structure,
Factory Method also qualifies as a design pattern: it has a name, the intent to decouple, and it introduces abstractions.
Note that the abstraction introduced by a design pattern is not necessarily introduced by means of a base class. As I will show you in the following sections and chapters, this abstraction can be introduced in many different ways, for instance, by means of templates or simply by function overloading. Again, a design pattern does not imply any specific implementation.
As a counter example, let us consider the std::make_unique()
function:
namespace
std
{
template
<
typename
T
,
typename
...
Args
>
unique_ptr
<
T
>
make_unique
(
Args
&&
...
args
);
}
// namespace std
In the C++ community, we often talk about the std::make_unique()
function
as
a factory function. It’s important to note that although the term factory function
gives the impression that std::make_unique()
is one example of the Factory Method
design pattern, this impression is incorrect. A design pattern helps you to decouple
by introducing an abstraction, which allows you to customize and defer implementation
details. In particular, the intent of the Factory Method design pattern is to introduce
a customization point for the purpose of object instantiation. std::make_unique()
does
not provide such a customization point: if you use std::make_unique()
, you know that
you will get a std::unique_ptr
to the type you are asking for and that the instance will
be created by means of new
:
// This will create a 'Widget' by means of calling 'new'
auto
ptr
=
std
::
make_unique
<
Widget
>
(
/* some Widget arguments */
);
Since std::make_unique()
doesn’t provide you with any way to customize that
behavior, it can’t help to reduce coupling between entities, and thus it cannot serve
the purpose of a design pattern.6 Still, std::make_unique()
is a recurring
solution for a specific problem. In other words, it is a pattern. However, it isn’t a design pattern but an implementation pattern. It is a popular solution
to encapsulate implementation details (in this case, the generation of an instance
of Widget
), but it does not abstract from what you get or how it will be created.
As such, it is part of the Implementation Details level but not the
Software Design level (refer back to Figure 1-1).
The introduction of abstractions is the key to decoupling software entities from one another
and to designing for change and extension. There is no abstraction in the std::make_unique()
function template, and thus no way for you to extend the functionality (you cannot even
properly overload or specialize). In contrast, the Factory Method design pattern
does provide an abstraction from what is created and how
this something is created (including actions before and after the instantiation). Due
to that abstraction you’ll be able to write new factories at a later point, without
having to change existing code. Therefore, the design pattern helps you decouple and
extend your software, while std::make_unique()
is only an implementation pattern.
Last but not least, a design pattern has been proven over the years. The Gang of Four did not collect all possible solutions, only solutions that were commonly used in different codebases to solve the same problem (although potentially with different implementations). Thus a solution has to demonstrate its value several times before it emerges as a pattern.
To summarize: a design pattern is a proven, named solution, which expresses a very specific intent. It introduces some kind of abstraction, which helps to decouple software entities and thus helps to manage the interaction between software entities. Just as we should use the term Design to denote the art of managing dependencies and decoupling (see “Guideline 1: Understand the Importance of Software Design”), we should use the term Design Pattern accurately and on purpose.
The last section focused on explaining the purpose of a design pattern: the combination of a name, an intent, and some form of abstraction to decouple software entities. However, just as it’s important to understand what a design pattern is, it’s important to understand what a design pattern is not. Unfortunately, there are several common misconceptions about design patterns:
Some consider design patterns as a goal and as a guarantee for achieving good software quality.
Some argue that design patterns are based on a particular implementation and thus are language-specific idioms.
Some say that design patterns are limited to object-oriented programming and dynamic polymorphism.
Some consider design patterns outdated or even obsolete.
These misconceptions come as no surprise since we rarely talk about design but instead focus on features and language mechanics (see “Guideline 1: Understand the Importance of Software Design”). For that reason, I will debunk the first three misconceptions in this guideline and will deal with the fourth one in the next section.
Some developers love design patterns. They are so infatuated with them that they try to solve all their problems by means of design patterns, whether it is reasonable or not. Of course, this way of thinking potentially increases the complexity of code and decreases comprehensibility, which may prove to be counterproductive. Consequently, this overuse of design patterns may result in frustration in other developers, in a bad reputation of design patterns in general, or even in rejection of the general idea of patterns.
To spell it out: design patterns are not a goal. They are a means to achieve a goal. They may be part of the solution. But they are not a goal. As Venkat Subramaniam would say: if you get up in the morning, thinking “What design pattern will I use today?”, then this is a telltale sign that you are missing the purpose of design patterns.7 There is no reward, no medal, for using as many design patterns as possible. The use of a design pattern shouldn’t create complexity but, on the contrary, decrease complexity. The code should become simpler, more comprehensible, and easier to change and maintain, simply because the design pattern should help to resolve dependencies and create a better structure. If using a design pattern leads to higher complexity and creates problems for other developers, it apparently isn’t the right solution.
Just to be clear: I’m not telling you not to use design patterns. I’m merely telling you not to overuse them, just as I would tell you not to overuse any other tool. It always depends on the problem. For instance, a hammer is a great tool, as long as your problem is nails. As soon as your problem changes to screws, a hammer becomes a somewhat inelegant tool.8 To properly use design patterns, to know when to use them and when not to use them, it’s so important to have a firm grasp of them, to understand their intent and structural properties, and to apply them wisely.
One of the most common misconceptions about design patterns is that they are based on a specific implementation. This includes the opinion that design patterns are more or less language-specific idioms. This misconception is easy to understand, as many design patterns, in particular the GoF patterns, are usually presented in an object-oriented setting and explained by means of object-oriented examples. In such a context, it’s easy to mistake the implementation details for a specific pattern and to assume that both are the same.
Fortunately, it’s also easy to demonstrate that design patterns are not about implementation details, any particular language feature, or any C++ standard. Let’s take a look at different implementations of the same design pattern. And yes, we will start with the classic, object-oriented version of the design pattern.
Consider the following scenario: we want to draw a given shape.9 The code snippet demonstrates this by means of a circle, but of course it
could be any other kind of shape, like a square or a triangle. For the purpose of drawing,
the Circle
class provides the draw()
member function:
class
Circle
{
public
:
void
draw
(
/*...*/
);
// Implemented in terms of some graphics library
// ...
};
It now appears self-evident that you need to implement the draw()
function. Without further
thought, you might do this by means of a common graphics library such as OpenGL, Metal,
Vulcan, or any other graphics library. However, it would be a big design flaw if the Circle
class
provides an implementation of the draw()
functionality itself: by implementing the draw()
function directly, you would introduce a strong coupling to your chosen graphics library. This
comes with a couple of downsides:
For every possible application of Circle
, you would always need the graphics library to
be available, even though you might not be interested in graphics but only need it as a
geometric primitive.
Every change to the graphics library might have an effect on the Circle
class, resulting
in necessary modifications, retesting, redeployment, etc.
Switching to another library in the future would mean everything but a smooth transition.
These problems all have a common source: implementing the draw()
function directly within the
Circle
class violates the Single-Responsibility Principle (SRP; see “Guideline 2: Design for Change”).
The class wouldn’t change for a single reason anymore and would strongly depend on that design
decision.
The classic object-oriented solution for this problem is to extract the decision about how to draw the circle and introduce an abstraction for that by means of a base class. Introducing such a variation point is the effect of the Strategy design pattern (see Figure 3-3).10
The intent of the Strategy design pattern is to define a family of algorithms and encapsulate
each one, therefore making them interchangeable. Strategy lets the algorithm vary independently
from clients that use it. By introducing the DrawStrategy
base class, it becomes possible to
easily vary the draw()
implementation of the given
Circle
. This also enables everyone, not
just you, to implement a new drawing behavior without modifying existing code and to inject it
from the outside into the
Circle
. This is what we commonly call dependency injection:
#include
<Circle.h>
#include
<OpenGLStrategy.h>
#include
<cstdlib>
#include
<utility>
int
main
()
{
// ...
// Creating the desired drawing strategy for a circle.
auto
strategy
=
std
::
make_unique_ptr
<
OpenGLStrategy
>
(
/* OpenGL-specific arguments */
);
// Injecting the strategy into the circle; the circle does not have to know
// about the specific kind of strategy, but can with blissful ignorance use
// it via the 'DrawStrategy' abstraction.
Circle
circle
(
4.2
,
std
::
move
(
strategy
)
);
circle
.
draw
(
/*...*/
);
// ...
return
EXIT_SUCCESS
;
}
This approach vastly increases the flexibility with respect to different drawing behavior: it
factors out all dependencies on specific libraries and other implementation details and thus
makes the code more changeable and extensible. For instance, it’s now easily possible to
provide a special implementation for testing purposes (i.e., a TestStrategy
). This
demonstrates that the improved flexibility has a very positive impact on the testability
of the design.
The Strategy design pattern is one of the classic GoF design patterns. As such, it is often referred to as an object-oriented design pattern and is often considered to require a base class. However, the intent of Strategy is not limited to object-oriented programming. Just as it’s possible to use a base class for the abstraction, it is just as easily possible to rely on a template parameter:
template
<
typename
DrawStrategy
>
class
Circle
{
public
:
void
draw
(
/*...*/
);
};
In this form, deciding how to draw the circle happens at compile time: instead of writing
a base class DrawStrategy
and passing a pointer to a DrawStrategy
at runtime, the
implementation details for drawing are provided by means of the DrawStrategy
template
argument. Note that while the template parameter allows you to inject the implementation
details from the outside, the Circle
is still not depending on any implementation details.
Therefore you have still decoupled the Circle
class from the used graphics library. In
comparison to the runtime approach, though, you will have to recompile every time the
DrawStrategy
changes.
While it’s true that the template-based solution fundamentally changes the properties of
the example (i.e., no base class and no virtual functions, no runtime decisions, no single
Circle
class, but one Circle
type for every concrete DrawStrategy
), it still
implements the intent of the Strategy design pattern perfectly. Thus this demonstrates
that a design pattern is not restricted to a particular implementation or a specific form
of abstraction.
Let’s consider another use case for the Strategy design pattern: the Standard Library
accumulate()
function template from the <numeric>
header:
std
::
vector
<
int
>
v
{
1
,
2
,
3
,
4
,
5
};
auto
const
sum
=
std
::
accumulate
(
begin
(
v
),
end
(
v
),
int
{
0
}
);
By default, std::accumulate()
sums up all elements in the given range. The third
argument specifies the initial value for the sum. Since std::accumulate()
uses the
type of that argument as the return type, the type of the argument is explicitly highlighted
as int{0}
instead of just 0
to prevent subtle misunderstandings. However, summing up
elements is only the tip of the iceberg: if you need to, you can specify how elements are
accumulated by providing a fourth argument to
std::accumulate()
. For instance, you could
use std::plus
or std::multiplies
from the
<functional>
header:
std
::
vector
<
int
>
v
{
1
,
2
,
3
,
4
,
5
};
auto
const
sum
=
std
::
accumulate
(
begin
(
v
),
end
(
v
),
int
{
0
},
std
::
plus
<>
{}
);
auto
const
product
=
std
::
accumulate
(
begin
(
v
),
end
(
v
),
int
{
1
},
std
::
multiplies
<>
{}
);
By means of the fourth argument, std::accumulate()
can be used for any kind
of
reduction operation, and thus the fourth argument represents the implementation of the
reduction operation. As such, it enables us to vary the implementation by injecting
the details of how the reduction should work from the outside.
std::accumulate()
therefore does not depend on a single, specific implementation but can be customized
by anyone to a specific purpose. This represents exactly the intent of the Strategy
design pattern.11
std::accumulate()
draws its power from a generic form of the Strategy design pattern.
Without the ability to change this behavior, it would be useful in only a very limited number
of use cases. Due to the Strategy design pattern, the number of possible uses is
endless.12
The example of std::accumulate()
demonstrates that design patterns, even the classic
GoF patterns, are not tied to one particular implementation and additionally are
not limited to object-oriented programming. Clearly the intent of many of these patterns
is also useful for other paradigms like functional or generic programming.13 Therefore, design patterns are not limited to
dynamic polymorphism, either. On the contrary: design patterns work equally well for
static polymorphism and can therefore be used in combination with C++ templates.
To further emphasize the point and to show you an additional example of the Strategy design
pattern, consider the declarations for the std::vector
and std::set
class templates:
namespace
std
{
template
<
class
T
,
class
Allocator
=
std
::
allocator
<
T
>
>
class
vector
;
template
<
class
Key
,
class
Compare
=
std
::
less
<
Key
>
,
class
Allocator
=
std
::
allocator
<
Key
>
>
class
set
;
}
// namespace std
All containers in the Standard Library (with the exception of std::array
) provide you with
the opportunity to specify a custom allocator. In the case of std::vector
it’s the second
template argument, and for std::set
it’s the third argument. All memory requests from the
container are handled via the given allocator.
By exposing a template argument for the allocator, the Standard Library containers give you the opportunity to customize memory allocation from the outside. They enable you to define a family of algorithms (in earlier case, an algorithm for the memory acquisition) and encapsulate each one and therefore make them interchangeable. Consequently you’re able to vary this algorithm independently from clients (in this case, the containers) that use it.14
Having read that description, you should recognize the Strategy design pattern. In this example, Strategy is again based on static polymorphism and implemented by means of a template argument. Clearly, Strategy is not limited to dynamic polymorphism.
While it’s obviously true that design patterns in general aren’t limited to object-oriented programming or dynamic polymorphism, I should still explicitly state that there are some design patterns whose intent is targeted to alleviate the usual problems in object-oriented programming (e.g., the Visitor and Prototype design patterns).15 And of course there are also design patterns focused on functional programming or generic programming (e.g., the Curiously Recurring Template Pattern [CRTP] and Expression Templates).16 While most design patterns are not paradigm centric and their intention can be used in a variety of implementations, some are more specific.
In the upcoming chapters, you’ll see examples for both categories. You will see design patterns that have a very general intent and are consequently of general usefulness. Additionally, you will see some design patterns that are more paradigm-specific and, due to that, will fail to be useful outside of their target domain. Still, they all have the main characteristics of design patterns in common: a name, an intent, and some form of abstraction.
In summary: design patterns are not limited to object-oriented programming, nor are they limited to dynamic polymorphism. More specifically, design patterns are not about a particular implementation and they are not language-specific idioms. Instead, they are focused entirely on the intent to decouple software entities in a specific way.
The previous section has demonstrated that design patterns are not limited to object-oriented programming or dynamic polymorphism, that they are not language-specific idioms, and that they are not about a particular implementation. Still, due to these common misconceptions and because we don’t consider C++ as solely object-oriented programming language anymore, some people even claim that design patterns are outdated or obsolete.17
I imagine you’re now looking a little skeptical. “Obsolete? Isn’t that a little exaggerated?” you ask. Well, unfortunately not. To tell a little war story, in early 2021 I had the honor of giving a virtual talk about design patterns in a German C++ user group. My main objective was to explain what design patterns are and that they are very much in use today. During the talk, I felt good, invigorated in my mission to help people see all the benefits of design patterns, and I sure gave my best to make everybody see the light that knowledge about design patterns brings. Still, a few days after the publication of the talk on YouTube, a user commented on the talk with “Really? Design Patterns in 2021?”
I very much hope that you are now shaking your head in disbelief. Yes, I could
not believe it either, especially after having shown that there are hundreds of
examples for design patterns in the C++ Standard Library. No, design
patterns are neither outdated nor obsolete. Nothing could be further from the truth.
To prove that design patterns are still very much alive and relevant, let’s
consider the updated allocators facility in the C++ Standard Library.
Take a look at the following code example that uses allocators from the
std::pmr
(polymorphic memory resource) namespace:
#
include
<array>
#
include
<cstddef>
#
include
<cstdlib>
#
include
<memory_resource>
#
include
<string>
#
include
<vector>
int
main
(
)
{
std
:
:
array
<
std
:
:
byte
,
1000
>
raw
;
// Note: not initialized!
std
:
:
pmr
:
:
monotonic_buffer_resource
buffer
{
raw
.
data
(
)
,
raw
.
size
(
)
,
std
:
:
pmr
:
:
null_memory_resource
(
)
}
;
std
:
:
pmr
:
:
vector
<
std
:
:
pmr
:
:
string
>
strings
{
&
buffer
}
;
strings
.
emplace_back
(
"
String longer than what SSO can handle
"
)
;
strings
.
emplace_back
(
"
Another long string that goes beyond SSO
"
)
;
strings
.
emplace_back
(
"
A third long string that cannot be handled by SSO
"
)
;
// ...
return
EXIT_SUCCESS
;
}
This example demonstrates how to use a
std::pmr::monotonic_buffer_resource
as allocator to redirect all memory allocations into
a predefined byte buffer. Initially we
are creating a buffer of 1,000 bytes in the form
of a std::array
(). This
buffer is provided as a source of memory to a
std::pmr::monotonic_buffer_resource
by means
of passing a pointer to the first element (via raw.data()
) and the size of the buffer
(via raw.size()
)
().
The third argument to the monotonic_buffer_resource
represents a backup allocator, which is
used in case the monotonic_buffer_resource
runs out of memory. Since we don’t need additional
memory in this case, we use the std::pmr::null_memory_resource()
function, which gives us a
pointer to the standard allocator that always fails to allocate. That means that you can ask
as nicely as you want, but the allocator returned by std::pmr::null_memory_resource()
will
always throw an exception when you ask for memory.
The created buffer is passed as allocator to the strings
vector, which will now acquire
all its memory from the initial byte buffer
().
Furthermore, since the vector forwards the allocator to its elements, even the three
strings, which we add by means of the emplace_back()
function and which are all too
long to rely on the Small String Optimization (SSO), will acquire all their memory
from the byte buffer. Thus, no dynamic memory is used in the entire example; all memory
will be taken from the byte array.18
At first glance, this example doesn’t look like it requires any design pattern to work. However, the allocator functionality used in this example uses at least four different design patterns: the Template Method design pattern, the Decorator design pattern, the Adapter design pattern, and (again) the Strategy design pattern.
There are even five design pattern if you count the Singleton pattern: the
null_memory_resource()
function
()
is implemented in terms of the Singleton pattern:19 it returns a pointer to a static storage duration object,
which is used to guarantee that there is at most one instance of this allocator.
All C++ allocators from the pmr
namespace, including the allocator returned
by null_memory_resource()
and the monotonic_buffer_resource
, are derived from the
std::pmr::memory_resource
base class. The first design pattern becomes visible
if you look at the memory_resource
class definition:
namespace
std
::
pmr
{
class
memory_resource
{
public
:
// ... a virtual destructor, some constructors and assignment operators
[[
nodiscard
]]
void
*
allocate
(
size_t
bytes
,
size_t
alignment
);
void
deallocate
(
void
*
p
,
size_t
bytes
,
size_t
alignment
);
bool
is_equal
(
memory_resource
const
&
other
)
const
noexcept
;
private
:
virtual
void
*
do_allocate
(
size_t
bytes
,
size_t
alignment
)
=
0
;
virtual
void
do_deallocate
(
void
*
p
,
size_t
bytes
,
size_t
alignment
)
=
0
;
virtual
bool
do_is_equal
(
memory_resource
const
&
other
)
const
noexcept
=
0
;
};
}
// namespace std::pmr
You may notice that the three functions in the public
section of the class have a virtual
counterpart in the private
section of the class. Whereas the public allocate()
,
deallocate()
, and is_equal()
functions represent the user-facing interface of the class,
the do_allocate()
, do_deallocate()
, and do_is_equal()
functions represent the interface
for derived classes. This separation of concerns is an example of the Non-Virtual Interface
(NVI) idiom, which itself is an example of the Template Method design
pattern.20
The second design pattern we implicitly use is the Decorator design pattern.21 Decorator helps you to build a hierarchical layer of allocators and to wrap and extend the functionality of one allocator to another. This idea becomes clearer in this line:
std
::
pmr
::
monotonic_buffer_resource
buffer
{
raw
.
data
(),
raw
.
size
(),
std
::
pmr
::
null_memory_resource
()
};
By passing the allocator returned by the null_memory_resource()
function to the
monotonic_buffer_resource
, we decorate its functionality. Whenever we ask the
monotonic_buffer_resource
for memory via the allocate()
function, it may forward the call to
its backup allocator. This way, we can implement many different kinds of allocators, which in
turn can be easily assembled to form a complete memory subsystem with different layers of
allocation strategies. This kind of combining and reusing pieces of functionality is the
strength of the Decorator design pattern.
You may have noticed that in the example code we have used std::pmr::vector
and
std::pmr::string
. I assume you remember that std::string
is just a type alias to
std::basic_string<char>
. Knowing that, it probably comes as no surprise that the two types
in the pmr
namespace are also just type aliases:
namespace
std
::
pmr
{
template
<
class
CharT
,
class
Traits
=
std
::
char_traits
<
CharT
>
>
using
basic_string
=
std
::
basic_string
<
CharT
,
Traits
,
std
::
pmr
::
polymorphic_allocator
<
CharT
>
>
;
template
<
class
T
>
using
vector
=
std
::
vector
<
T
,
std
::
pmr
::
polymorphic_allocator
<
T
>
>
;
}
// namespace std::pmr
These type aliases still refer to the regular std::vector
and std::basic_string
classes but
do not expose a template parameter for an allocator anymore. Instead, they employ a
std::pmr::polymorphic_allocator
as allocator. This is an example of
the Adapter design pattern.22 The intent of an Adapter is to
help you to glue two nonfitting interfaces together. In this case, the
polymorphic_allocator
helps to transmit between the classic, static interface required from the classic C++
allocators and the new, dynamic allocator interface required by std::pmr::memory_resource
.
The fourth and last design pattern used in our example is, again, the Strategy design pattern.
By exposing a template argument for the allocator, Standard Library containers like std::vector
and std::string
give you the opportunity to customize memory allocation from outside. This is
a static form of the Strategy design pattern and has the same intent as customizing algorithms
(see also “Guideline 12: Beware of Design Pattern Misconceptions”).
This example impressively demonstrates, that design patterns are far from being obsolete. On closer examination, we see them everywhere: any kind of abstraction and any attempt to decouple software entities and introduce flexibility and extensibility is very likely based on some design pattern. For that reason, it definitely helps to know about the different design patterns and to understand their intent to recognize them and apply them whenever it is necessary and appropriate.
In the last two sections, you learned what a design pattern is, what it’s not, and that design patterns are everywhere. You also learned that every design pattern has a name, which expresses a clear, concise, and unambiguous intent. Hence, the name carries meaning.23 By using the name of a design pattern you can express what the problem is and which solution you’ve chosen to solve the problem, and you can describe how the code is expected to evolve.
Consider, for instance, the Standard Library accumulate()
function:
template
<
class
InputIt
,
class
T
,
class
BinaryOperation
>
constexpr
T
accumulate
(
InputIt
first
,
InputIt
last
,
T
init
,
BinaryOperation
op
);
The third template parameter is named BinaryOperation
. While this does communicate the fact
that the passed callable is required to take two arguments, the name does not communicate the
intent of the parameter. To express the intent more clearly, consider calling it
BinaryReductionStrategy
:
template
<
class
InputIt
,
class
T
,
class
BinaryReductionStrategy
>
constexpr
T
accumulate
(
InputIt
first
,
InputIt
last
,
T
init
,
BinaryReductionStrategy
op
);
Both the term Reduction and the name Strategy carry meaning for every C++
programmer. Therefore, you’ve now captured and expressed your intent much more clearly:
the parameter enables dependency injection of a binary operation, which allows you to
specify how the reduction operation works. Therefore, the parameter solves the problem
of customization. Still, as you will see in Chapter 5,
the Strategy design pattern communicates that there are certain expectations for the
operation. You can only specify how the reduction operation works; you cannot redefine
what accumulate()
does. If that’s what you want to express, you should use the name
of the Command design pattern:24
template
<
class
InputIt
,
class
UnaryCommand
>
constexpr
UnaryCommand
for_each
(
InputIt
first
,
InputIt
last
,
UnaryCommand
f
);
The std::for_each()
algorithm allows you to apply any kind of unary operation to a range of
elements. To express this intent, the second template parameter could be named UnaryCommand
,
which unambiguously expresses that there are (nearly) no expectations for the operation.
Another example from the Standard Library shows how much value the name of a design pattern can bring to a piece of code:
#
include
<cstdlib>
#
include
<iostream>
#
include
<string>
#
include
<variant>
struct
{
void
operator
(
)
(
int
i
)
const
{
std
:
:
cout
<
<
"
int:
"
<
<
i
<
<
'
\n
'
;
}
void
operator
(
)
(
double
d
)
const
{
std
:
:
cout
<
<
"
double:
"
<
<
d
<
<
'
\n
'
;
}
void
operator
(
)
(
std
:
:
string
const
&
s
)
const
{
std
:
:
cout
<
<
"
string:
"
<
<
s
<
<
'
\n
'
;
}
}
;
int
main
(
)
{
std
:
:
variant
<
int
,
double
,
std
:
:
string
>
v
{
}
;
v
=
"
C++ Variant example
"
;
std
:
:
visit
(
{
}
,
v
)
;
return
EXIT_SUCCESS
;
}
In the main()
function, we create a std::variant
for the three alternatives int
,
double
, and std::string
().
In the next line, we assign a C-style string literal, which will be converted to a
std::string
inside the variant
().
Then we print the content of the variant via the std::visit()
function and the Print
function object
().
Notice the name of the std::visit()
function. The name directly refers to the Visitor design
pattern and therefore clearly expresses its intent: you’re able to apply any operation to the
closed set of types contained in the variant instance.25 Also, you can extend the set of operations nonintrusively.
You see that using the name of a design pattern carries more information than using an arbitrary name. Still, this shouldn’t imply that naming is easy.26 A name should primarily help you understand the code in a specific context. If the name of a design pattern can help with that, then consider including the design pattern name to express your intent.
1 The Gang of Four, or simply GoF, is a commonly used reference to the four authors Erich Gamma, Richard Helm, Ralph E. Johnson, and John Vlissides and their book on design patterns: Design Patterns: Elements of Reusable Object-Oriented Software (Prentice Hall). The GoF book still is, after several decades, the reference on design patterns. Throughout the rest of this book, I will refer to either the GoF book, the GoF patterns, or the characteristic, object-oriented GoF style.
2 If you do not know the Visitor design pattern yet, don’t worry. I will introduce the pattern in Chapter 4.
3 The Strategy design pattern will be explained in detail in Chapter 5, the Decorator design pattern in Chapter 9.
4 I mention only the design patterns that I will explain in later chapters (see the Strategy and Command design patterns in Chapter 5 and the Bridge design pattern in “Guideline 28: Build Bridges to Remove Physical Dependencies”). There are a few more design patterns that share the same structure.
5 If you are unfamiliar with the Strategy design pattern, rest assured that Chapter 5 will provide much more information, including several code examples.
6 This may be a controversial example. Since I know the C++ community, I know that you may have a different opinion. However, I stand by mine: due to its definition, std::make_unique()
is incapable of decoupling software entities and therefore does not play a role on the level of software design. It’s merely an implementation detail (but a valuable and useful one).
7 Venkat Subramaniam and Andrew Hunt, Practices of an Agile Developer (The Pragmatic Programmers, LLC, 2017).
8 Well, it works, in some definition of “works.”
9 I know what you’re thinking: “You cannot be serious! There is so many interesting examples out there, but you select the oldest and most boring example in the book!” OK, I admit that might not be the most exciting example to pick. But, still, I have two good reasons to use this example. First, the scenario is so well known that I can assume that no one has trouble understanding it. That means that everyone should be able to follow my arguments about software design. And second, let’s agree that it’s kind of a tradition in computer science to start with a shape or an animal example. And, of course, I do not want to disappoint traditionalists.
10 Chapter 5 will provide a complete and thorough introduction of the Strategy design pattern.
11 You may (correctly) observe that even without the fourth argument you could change how the accumulation works by providing a custom addition operator (i.e., operator+()
) for the given type. However, that is only of limited use. While you can provide a custom addition operator for user-defined types, you cannot provide a custom addition operator for fundamental types (such as the int
in the example). Also, it’s very questionable to define operator+()
for anything other than an addition operation (or related operations like the concatenation of strings). Thus, relying on the addition operator would be limiting technically and semantically.
12 In his CppCon 2016 talk “std::accumulate: Exploring an Algorithmic Empire”, Ben Deane has impressively demonstrated how powerful std::accumulate()
is thanks to that fourth argument.
13 For more information about STL algorithms and their functional programming heritage, see Ivan Cukic’s excellent introduction to Functional Programming in C++ (Manning).
14 Another commonly used name for that form of the Strategy design pattern is Policy-Based Design; see “Guideline 19: Use Strategy to Isolate How Things Are Done”.
15 I will explain the Visitor design pattern in Chapter 4 and the Prototype design pattern in “Guideline 30: Apply Prototype for Abstract Copy Operations”.
16 Again, I’m referring you to Ivan Cukic’s introduction to Functional Programming in C++. The CRTP design pattern will be the topic of “Guideline 26: Use CRTP to Introduce Static Type Categories”. For information on Expression Templates, a template-based pattern, refer to the C++ template reference: David Vandevoorde, Nicolai Josuttis, and Douglas Gregor’s C++ Templates: The Complete Guide (Addison-Wesley).
17 I would argue that C++ has been a multiparadigm programming language since the moment the first implementation of templates was added to the language in 1989. The impact of templates on the language became clear with the addition of part of the Standard Template Library (STL) to the Standard Library in 1994. Since then, C++ has provided object-oriented, functional, and generic capabilities.
18 The Small String Optimization (SSO) is a common optimization for small strings. Instead of allocating dynamic memory on the heap via the provided allocator, the string would store the small number of characters directly into the stack part of the string. Since a string usually occupies between 24 and 32 bytes on the stack (which is not a C++ standard requirement but a property of common implementations of std::string
), anything beyond 32 bytes will require a heap allocation. That is the case with the three given strings.
19 Singleton is one of the original 23 GoF design patterns. But I will do my best in “Guideline 37: Treat Singleton as an Implementation Pattern, Not a Design Pattern” to convince you that Singleton is not actually a design pattern but an implementation detail. For that reason, I will refer to Singleton not as a design pattern but simply as an implementation pattern.
20 Unfortunately, I won’t cover the Template Method design pattern in this book. This isn’t because it’s not important but simply due to a lack of available pages. Please refer to the GoF book for more details.
21 I will give a complete introduction of the Decorator design pattern in Chapter 9.
22 The Adapter design pattern will be the topic of “Guideline 24: Use Adapters to Standardize Interfaces”.
23 Good names always carry meaning. This is why they are so fundamentally important.
24 I will explain the Command design pattern alongside the Strategy design pattern in Chapter 5.
25 The Visitor design pattern, including the modern implementation with std::variant
, will be our focus in Chapter 4.
26 Naming is hard, as Kate Gregory aptly remarks in her highly recommended talk “Naming Is Hard: Let’s Do Better” at CppCon 2019.