Chapter 9. The Decorator Design Pattern

This chapter is dedicated to another classic design pattern: the Decorator design pattern. Over the years, Decorator has proven to be one of the most useful design patterns when it comes to combining and reusing different implementations. So it doesn’t come as a surprise that it is commonly used, even for one of the most impressive reworks of a C++ Standard Library feature. My primary objective in this chapter will be to give you a very good idea why, and when, Decorator is a great choice for designing software. Additionally, I will show you the modern, more value-based forms of Decorator.

In “Guideline 35: Use Decorators to Add Customization Hierarchically”, we will dive into the design aspects of the Decorator design pattern. You will see when it is the right design choice and which benefits you’re gaining by using it. Additionally, you will learn about differences compared to other design patterns and its potential shortcomings.

In “Guideline 36: Understand the Trade-off Between Runtime and Compile Time Abstraction”, we will take a look at two more implementations of the Decorator design pattern. Although both implementations will be firmly rooted in the realm of value semantics, the first one will be based on static polymorphism, while the second one will be based on dynamic polymorphism. Even though both have the same intent and thus implement Decorator, the contrast between these two will give you an impression of the vastness of the design space for design patterns.

Guideline 35: Use Decorators to Add Customization Hierarchically

Ever since you solved the design problem of your team’s 2D graphics tool by proposing a solution based on the Strategy design pattern (remember “Guideline 19: Use Strategy to Isolate How Things Are Done”), your reputation as design pattern expert has spread across the company. Therefore, it does not come as a surprise that other teams are seeking you out for guidance. One day, two developers of your companies merchandise management system come to your office and ask for your help.

Your Coworkers’ Design Issue

The team of the two developers is dealing with a lot of different Items (see Figure 9-1). All of these items have one thing in common: they have a price() tag. The two developers try to explain their problem by means of two items taken from the C++ merchandise shop: a class representing a C++ book (the CppBook class) and a C++ conference ticket (the ConferenceTicket class).

The initial +Item+ inheritance hierarchy.
Figure 9-1. The initial Item inheritance hierarchy

As the developers sketch their problem, you start to understand that their problem appears to be the many different ways to modify a price. Initially, they tell you, they only had to take taxes into account. For that reason, the Item base class was equipped with a protected data member to represent the tax rate:

//---- <Money.h> ----------------

class Money { /*...*/ };

Money operator*( Money money, double factor );
Money operator+( Money lhs, Money rhs );


//---- <Item.h> ----------------

#include <Money.h>

class Item
{
 public:
   virtual ~Item() = default;

   virtual Money price() const = 0;
   // ...

 protected:
   double taxRate_;
};

This apparently worked well for some time, until one day, when they were asked to also take different rates of discount into account. This apparently required a lot of effort to refactor the large amount of the existing classes for their numerous different items. You can easily imagine that this was necessary because all derived classes were accessing the protected data members. “Yes, you should always design for change…” you think to yourself.1

They continue by admitting to their unfortunate misdesign. Of course they should have done a better job of encapsulating the tax rates in the Item base class. However, along with this realization came the understanding that when representing price modifiers by data members in the base class, any new kind of price modifier would always be an intrusive action and would always directly affect the Item class. For that reason, they started to think about how to avoid this kind of major refactoring in the future and how to enable the easy addition of new modifiers. “That’s the way to go!” you think to yourself. Unfortunately, the first approach that came to their mind was to factor out the different kinds of price modifiers by means of an inheritance hierarchy (see Figure 9-2).

The extended +Item+ inheritance hierarchy.
Figure 9-2. The extended Item inheritance hierarchy

Instead of encapsulating the tax and discount values inside the base class, these modifiers are factored out into derived classes, which perform the required price adaptation. “Uh-oh…” you start to think. Apparently your look already gives away that you are not particularly fond of this idea, and so they are quick to tell you that they have already discarded the idea. Obviously they have realized on their own that this would cause even more problems: this solution would quickly cause an explosion of types and would provide only poor reuse of functionality. Unfortunately, a lot of code would be doubled, since for every specific Item, the code for taxes and discounts had to be duplicated. Most troublesome, however, would be the handling of Items that are affected both by tax and some sort of discount: they neither liked the approach to provide classes to handle both, nor did they want to introduce another layer in the inheritance hierarchy (see Figure 9-3).

The problematic +Item+ inheritance hierarchy.
Figure 9-3. The problematic Item inheritance hierarchy

Apparently, and surprising for them, they couldn’t deal with the price modifiers in the base class or in the derived classes by means of direct inheritance. However, before you have the opportunity to make any comments about separating concerns, they explain that they have recently heard about your Strategy solution. This finally gave them an idea how to properly refactor the problem (see Figure 9-4).

By extracting the price modifiers into a separate hierarchy, and by configuring Items upon construction by means of a PriceStrategy, they had finally found a working solution to nonintrusively add new price modifiers, which will save them a lot of refactoring work. “Well, this is the benefit of separating concerns and favoring composition over inheritance,” you think to yourself.2 And aloud you ask, “This is great, I’m really happy for you. Everything seems to work now, you’ve figured it out on your own! Why exactly are you here?”

The Strategy-based +Item+ inheritance hierarchy.
Figure 9-4. The Strategy-based Item inheritance hierarchy

They tell you that your Strategy solution is by far the best approach they have (thankful looks included). However, they admit that they are not entirely happy with the approach. From their point of view, two problems remain and, of course, they are hoping that you have an idea how to fix them. The first issue they see is that every Item instance needs a Strategy class, even if no price modifier applies. While they agree that this can be solved by some kind of null object, they feel that there should be a simpler solution:3

class PriceStrategy
{
 public:
   virtual ~PriceStrategy() = default;
   virtual Money update( Money price ) const = 0;
   // ...
};

class NullPriceStrategy : public PriceStrategy
{
 public:
   Money update( Money price ) const override { return price; }
};

The second problem they have appears to be a little more difficult to solve. Obviously they are interested in combining different kinds of modifiers (e.g., Discount and Tax into DiscountAndTax). Unfortunately, they experience some code duplication in their current implementation. For instance, both the Tax and the DiscountAndTax classes contain tax-related computations. And while right now, with only the two modifiers, there are reasonable solutions at hand to cope with the duplication, they are anticipating problems when adding more modifiers and arbitrary combinations of these. Therefore they are wondering if there is another, better solution for dealing with different kinds of price modifiers.

This is indeed an intriguing problem, and you are happy to have taken the time to help them. They are absolutely correct: the Strategy design pattern is not the right solution for this problem. While Strategy is a great solution to remove dependencies on the complete implementation details of a function and to handle different implementations gracefully, it does not enable the easy combination and reuse of different implementations. Attempting to do this would quickly result in an undesirably complex Strategy inheritance hierarchy.

What they need for their problem appears to be more like a hierarchical form of Strategy, a form that decouples the different price modifiers but also allows for a very flexible combination of them. Hence, one key to success is a consequent application of the separation of concerns: any rigid, manually encoded combination in the spirit of a DiscountAndTax class would be prohibitive. However, the solution should also be nonintrusive to enable them to implement new ideas at any time without the need to modify existing code. And finally, it should not be necessary to handle a default case by some artificial null object. Instead, it would be more reasonable to consequently build on composition instead of inheritance and implement a price modifier in the form of a wrapper. With this realization, you start to smile. Yes, there is just the right design pattern for this purpose: what your two guests need is an implementation of the Decorator design pattern.

The Decorator Design Pattern Explained

The Decorator design pattern also originates from the GoF book. Its primary focus is the flexible combination of different pieces of functionality through composition:

The Decorator Design Pattern

Intent: “Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.”4

Figure 9-5 shows the UML diagram for the given Item problem. As before, the Item base class represents the abstraction from all possible items. The deriving CppBook class, on the other hand, acts as a representative for different implementations of Item. The problem in this hierarchy is the difficult addition of new modifiers for the existing price() function(s). In the Decorator design pattern, this addition of new “responsibilities” is identified as a variation point and extracted in the form of the DecoratedItem class. This class is a separate, special implementation of the Item base class and represents an added responsibility for any given item. On the one hand, a DecoratedItem derives from Item and hence must adhere to all expectations of the Item abstraction (see “Guideline 6: Adhere to the Expected Behavior of Abstractions”). On the other hand, it also contains an Item (either through composition or aggregation). Due to that, a DecoratedItem acts as a wrapper around each and every item, potentially one that itself can extend the functionality. For that reason, it provides the foundation for a hierarchical application of modifiers. Two possible modifiers are represented by the Discounted class, which represents a discount for a specific item, and the Taxed class, which represents some kind of tax.5

The UML representation of the _Decorator_ design pattern.
Figure 9-5. The UML representation of the Decorator design pattern

By introducing the DecoratedItem class and separating the aspect that’s required to change, you adhere to the SRP. By separating this concern and therefore allowing the easy addition of new price modifiers, you also adhere to the Open-Closed Principle (OCP). Due to the hierarchical, recursive nature of the DecoratedItem class, and due to the gained ability to reuse and combine different modifiers easily, you also follow the advice of the Don’t Repeat Yourself (DRY) principle. Last but not least, because of the wrapper approach of Decorator, there’s no need to define any default behavior in the form of a null object. Any Item that does not require a modifier can be used as is.

Figure 9-6 illustrates the dependency graph of the Decorator design pattern. In this figure, the Item class resides on the highest level of the architecture. All other classes depend on it, including the DecoratedItem class, which resides one level below. Of course, this is not a requirement: it’s perfectly acceptable if both the Item and the DecoratedItem are introduced on the same architectural level. However, this example demonstrates that it’s always possible (anytime, anywhere) to introduce a new Decorator without needing to modify existing code. The concrete types of Items are implemented on the lowest level of the architecture. Note that there is no dependency between these items: all items, including modifiers like Discounted, can be introduced independently by anyone at any time and, due to the structure of Decorator, be flexibly and arbitrarily combined.

The dependency graph for the _Decorator_ design pattern.
Figure 9-6. Dependency graph for the Decorator design pattern

A Classic Implementation of the Decorator Design Pattern

Let’s take a look at a complete, GoF-style implementation of the Decorator design pattern by means of the given Item example:

//---- <Item.h> ----------------

#include <Money.h>

class Item
{
 public:
   virtual ~Item() = default;
   virtual Money price() const = 0;
};

The Item base class represents the abstraction for all possible items. The only requirement is defined by the pure virtual price() function, which can be used to query for the price of the given item. The DecoratedItem class represents one possible implementation of the Item class (1):

//---- <DecoratedItem.h> ----------------

#include <Item.h>
#include <memory>
#include <stdexcept>
#include <utility>

class DecoratedItem : public Item  1
{
 public:
   explicit DecoratedItem( std::unique_ptr<Item> item )  3
      : item_( std::move(item) )
   {
      if( !item_ ) {
         throw std::invalid_argument( "Invalid item" );
      }
   }

 protected:
   Item&       item()       { return *item_; }  4
   Item const& item() const { return *item_; }

 private:
   std::unique_ptr<Item> item_;  2
};

A DecoratedItem derives from the Item class but also contains an item_ (2). This item_ is specified via the constructor, which accepts any non-null std::unique_ptr to another Item (3). Note that this DecoratedItem class is still abstract, since the pure virtual price() function is not yet defined. DecoratedItem provides only the necessary functionality to store an Item and to access the Item via protected member functions (4).

Equipped with these two classes, it’s possible to implement concrete Items:

//---- <CppBook.h> ----------------

#include <Item.h>
#include <string>
#include <utility>

class CppBook : public Item  5
{
 public:
   CppBook( std::string title, Money price )
      : title_{ std::move(title) }
      , price_{ price }
   {}

   std::string const& title() const { return title_; }
   Money price() const override { return price_; }

 private:
   std::string title_{};
   Money price_{};
};


//---- <ConferenceTicket.h> ----------------

#include <Item.h>
#include <string>
#include <utility>

class ConferenceTicket : public Item  6
{
 public:
   ConferenceTicket( std::string name, Money price )
      : name_{ std::move(name) }
      , price_{ price }
   {}

   std::string const& name() const { return name_; }
   Money price() const override { return price_; }

 private:
   std::string name_{};
   Money price_{};
};

The CppBook and ConferenceTicket classes represent possible specific Item implementations (5 and 6). While a C++ book is represented by means of the title of the book, a C++ conference is represented by means of the name of the conference. Most importantly, both classes override the price() function by returning the specified price_.

Both CppBook and ConferenceTicket are oblivious to any kind of tax or discount. But obviously, both kinds of Item are potentially subject to both. These price modifiers are implemented by means of the Discounted and Taxed classes:

//---- <Discounted.h> ----------------

#include <DecoratedItem.h>

class Discounted : public DecoratedItem
{
 public:
   Discounted( double discount, std::unique_ptr<Item> item )  7
      : DecoratedItem( std::move(item) )
      , factor_( 1.0 - discount )
   {
      if( !std::isfinite(discount) || discount < 0.0 || discount > 1.0 ) {
         throw std::invalid_argument( "Invalid discount" );
      }
   }

   Money price() const override
   {
      return item().price() * factor_;  8
   }

 private:
   double factor_;
};

The Discounted class (7) is initialized by passing a std::unique_ptr to an Item and a discount value, represented by a double value in the range of 0.0 to 1.0. While the given Item is immediately passed to the DecoratedItem base class, the given discount value is used to compute a discount factor_. This factor is used in the implementation of the price() function to modify the price of the given item (8). This can either be a specific item like CppBook or ConferenceTicket or any Decorator like Discounted, which in turn modifies the price of another Item. Thus, the price() function is the point where the hierarchical structure of Decorator is fully exploited.

//---- <Taxed.h> ----------------

#include <DecoratedItem.h>

class Taxed : public DecoratedItem
{
 public:
   Taxed( double taxRate, std::unique_ptr<Item> item )  9
      : DecoratedItem( std::move(item) )
      , factor_( 1.0 + taxRate )
   {
      if( !std::isfinite(taxRate) || taxRate < 0.0 ) {
         throw std::invalid_argument( "Invalid tax" );
      }
   }

   Money price() const override
   {
      return item().price() * factor_;
   }

 private:
   double factor_;
};

The Taxed class is very similar to the Discounted class. The major difference is the evaluation of a tax-related factor in the constructor (9). Again, this factor is used in the price() function to modify the price of the wrapped Item.

All of this functionality is put together in the main() function:

#include <ConferenceTicket.h>
#include <CppBook.h>
#include <Discounted.h>
#include <Taxed.h>
#include <cstdlib>
#include <memory>

int main()
{
   // 7% tax: 19*1.07 = 20.33
   std::unique_ptr<Item> item1(  10
      std::make_unique<Taxed>( 0.07,
         std::make_unique<CppBook>( "Effective C++", 19.0 ) ) );

   // 20% discount, 19% tax: (999*0.8)*1.19 = 951.05
   std::unique_ptr<Item> item2(  11
      std::make_unique<Taxed>( 0.19,
         std::make_unique<Discounted>( 0.2,
            std::make_unique<ConferenceTicket>( "CppCon", 999.0 ) ) ) );

   Money const totalPrice1 = item1->price();  // Results in 20.33
   Money const totalPrice2 = item2->price();  // Results in 951.05

   // ...

   return EXIT_SUCCESS;
}

As a first Item, we create a CppBook. Let’s assume that this book is subject to a 7% tax, which is applied by means of wrapping a Taxed decorator around the item. The resulting item1 therefore represents a taxed C++ book (10). As a second Item, we create a ConferenceTicket instance, which represents CppCon. We were lucky to get one of the early-bird tickets, which means that we are granted a discount of 20%. This discount is wrapped around the ConferenceTicket instance by means of the Discounted class. The ticket is also subject to 19% tax, which, as before, is applied via the Taxed decorator. Hence, the resulting item2 represents a discounted and taxed C++ conference ticket (11).

A Second Decorator Example

Another, impressive example that shows the benefits of the Decorator design pattern can be found in the C++17 rework of the STL allocators. Since the allocators’ implementation is based on Decorator, it’s possible to create arbitrarily complex hierarchies of allocators, which fulfill even the most special of memory requirements. Consider, for instance, the following example using a std::pmr::monotonic_buffer_resource (12):

#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() }; 12

   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;
}

The std::pmr::monotonic_buffer_resource is one of several available allocators in the std::pmr namespace. In this example, it’s configured such that whenever the strings vector asks for memory, it will dispense only chunks of the given byte array raw. Memory requests that cannot be handled, for instance because the buffer is out of memory, are dealt with by throwing a std::bad_alloc exception. This behavior is specified by passing a std::pmr::null_memory_resource during construction. There are many other possible applications for a std::pmr::monotonic_buffer_resource, though. For instance, it would also be possible to build on dynamic memory and to let it reallocate additional chunks of memory via new and delete by means of std::pmr::new_delete_resource() (13):

// ...

int main()
{
   std::pmr::monotonic_buffer_resource
      buffer{ std::pmr::new_delete_resource() };  13

   // ...
}

This flexibility and hierarchical configuration of allocators is made possible by means of the Decorator design pattern. The std::pmr::​monotonic_buffer_resource is derived from the std::pmr::memory_resource base class but, at the same time, also acts as a wrapper around another allocator derived from std::pmr::memory_resource. The upstream allocator, which is used whenever the buffer goes out of memory, is specified on construction of a std::pmr::monotonic_buffer_resource.

Most impressive, however, is that you can easily and nonintrusively customize the allocation strategy. That might, for instance, be interesting to enable you to deal with requests for large chunks of memory differently than requests for small chunks. All you have to do is to provide your own, custom allocator. Consider the following sketch of a CustomAllocator:

//---- <CustomAllocator.h> ----------------

#include <cstdlib>
#include <memory_resource>

class CustomAllocator : public std::pmr::memory_resource  14
{
 public:
   CustomAllocator( std::pmr::memory_resource* upstream )  16
      : upstream_{ upstream }
   {}

 private:
   void* do_allocate( size_t bytes, size_t alignment ) override;  17

   void do_deallocate( void* ptr, [[maybe_unused]] size_t bytes,  18
                       [[maybe_unused]] size_t alignment ) override;

   bool do_is_equal(
      std::pmr::memory_resource const& other ) const noexcept override;  19

   std::pmr::memory_resource* upstream_{};  15
};

To be recognized as a C++17 allocator, the CustomAllocator class derives from the std::pmr::memory_resource class, which represents the set of requirements for all C++17 allocators (14). Coincidentally, the CustomAllocator also owns a pointer to a std::pmr::memory_resource (15), which is initialized via its constructor (16).

The set of requirements for C++17 allocators consists of the virtual functions do_allocate(), do_deallocate(), and do_is_equal(). The do_allocate() function is responsible for acquiring memory, potentially via its upstream allocator (17), while the do_deallocate() function is called whenever memory needs to be given back (18). Last but not least, the do_is_equal() function is called whenever the equality of two allocators needs to be checked (19).6

By just introducing the CustomAllocator and without the need to change any other code, in particular in the Standard Library, the new kind of allocator can be easily plugged in between the std::pmr::monotonic_buffer_resource and the std::pmr::new_delete_resource() (20), thus allowing you to nonintrusively extend the allocation behavior:

// ...
#include <CustomAllocator.h>

int main()
{
   CustomAllocator custom_allocator{ std::pmr::new_delete_resource() };

   std::pmr::monotonic_buffer_resource buffer{ &custom_allocator };  20

   // ...
}

Comparison Between Decorator, Adapter, and Strategy

With the names Decorator and Adapter, these two design patterns sound like they have a similar purpose. On closer examination, however, these two patterns are very different and hardly related at all. The intent of the Adapter design pattern is to adapt and change a given interface to an expected interface. It is not concerned about adding any functionality but only about mapping one set of functions onto another (see also “Guideline 24: Use Adapters to Standardize Interfaces”). The Decorator design pattern, on the other hand, preserves a given interface and isn’t at all concerned about changing it. Instead, it provides the ability to add responsibilities and to extend and customize an existing set of functions.

The Strategy design pattern is much more like Decorator. Both patterns provide the ability to customize functionality. However, both patterns are intended for different applications and therefore provide different benefits. The Strategy design pattern is focused on removing the dependencies on the implementation details of a specific functionality and enables you to define these details from the outside. Thus from this perspective, it represents the core—the “guts”—of this functionality. This form makes it particularly suited to represent different implementations and to switch between them (see “Guideline 19: Use Strategy to Isolate How Things Are Done”). In comparison, the Decorator design pattern is focused on removing the dependency between attachable pieces of implementation. Due to its wrapper form, Decorator represents the “skin” of a functionality.7 In this form, it is particularly well suited to combine different implementations, which enables you to augment and extend functionality, rather than replacing it or switching between implementations.

Obviously, both Strategy and Decorator have their individual strengths and should be selected accordingly. However, it’s also possible to combine these two design patterns to gain the best of both worlds. For instance, it would be possible to implement Items in terms of the Strategy design patterns but allow for a more fine-grained configuration of Strategy by means of Decorator:

class PriceStrategy
{
 public:
   virtual ~PriceStrategy() = default;
   virtual Money update( Money price ) const = 0;
   // ...
};

class DecoratedPriceStrategy : public PriceStrategy
{
 public:
   // ...
 private:
   std::unique_ptr<PriceStrategy> priceModifier_;
};

class DiscountedPriceStrategy : public DecoratedPriceStrategy
{
 public:
   Money update( Money price ) const override;
   // ...
};

This combination of design patterns is particularly interesting if you already have a Strategy implementation in place: while Strategy is intrusive and requires the modification of a class, it’s always possible to nonintrusively add a Decorator such as the DecoratedPriceStrategy class. But of course it depends: whether or not this is the right solution is something you’ll have to decide on a case-by-case basis.

Analyzing the Shortcomings of the Decorator Design Pattern

With its ability to hierarchically extend and customize behavior, the Decorator design pattern is clearly one of the most valuable and flexible patterns in the catalogue of design patterns. However, despite its benefits, it also comes with a couple of disadvantages. First and foremost, the flexibility of a Decorator comes with a price: every level in a given hierarchy adds one level of indirection. As a specific example, in the object-oriented implementation of the Item hierarchy, this indirection comes in the form of one virtual function call per Decorator. Thus an extensive use of Decorators may incur a potentially significant performance overhead. Whether or not this possible performance penalty poses a problem depends on the context. You’ll have to decide from case to case using benchmarks to determine whether the flexibility and the structural aspects of Decorator outweigh the performance problem.

Another shortcoming is the potential danger of combining Decorators in a nonsensical way. For instance, it’s easily possible to wrap a Taxed Decorator around another Taxed Decorator or to apply a Discounted on an already-taxed Item. Both scenarios would make your government happy but still should never happen and therefore should be avoided by design. This rational is nicely expressed by Scott Meyers’s universal design principle:8

Make interfaces easy to use correctly and hard to use incorrectly.

Thus the enormous flexibility of Decorators is extraordinary, but can also be dangerous (depending on the scenario, of course). Since in this scenario taxes appear to play a special role, it seems to be very reasonable not to deal with them as Decorator, but differently. Since in reality taxes turn out to be a rather complex topic, it appears to be reasonable to separate this concern via the Strategy design pattern:

//---- <TaxStrategy.h> ----------------

#include <Money.h>

class TaxStrategy  21
{
 public:
   virtual ~TaxStrategy() = default;
   virtual Money applyTax( Money price ) const = 0;
   // ...
};


//---- <TaxedItem.h> ----------------

#include <Money.h>
#include <TaxStrategy.h>
#include <memory>

class TaxedItem
{
 public:
   explicit TaxedItem( std::unique_ptr<Item> item
                     , std::unique_ptr<TaxStrategy> taxer )  22
      : item_( std::move(item) )
      , taxer_( std::move(taxer) )
   {
      // Check for a valid item and tax strategy
   }

   Money netPrice() const  // Price without taxes  23
   {
      return price();
   }

   Money grossPrice() const  // Price including taxes  24
   {
      return taxer_.applyTax( item_.price() );
   }

 private:
   std::unique_ptr<Item> item_;
   std::unique_ptr<TaxStrategy> taxer_;
};

The TaxStrategy class represents the many different ways to apply taxes to an Item (21). Such a TaxStrategy is combined with an Item in the TaxedItem class (22). Note that TaxedItem is not an Item itself and therefore cannot be decorated by means of another Item. It therefore serves as a kind of terminating Decorator, which can only be applied as the very last decorator. It also does not provide a price() function: instead, it provides the netPrice() (23) and grossPrice() (24) functions to enable queries for both the price including taxes and the original price of the wrapped Item.9

The only other problem that you might see is the reference semantics–based implementation of the Decorator design pattern: lots of pointers, including nullptr checks and the danger of dangling pointers, explicit lifetime management by means of std::unique_ptr and std::make_unique(), and the many small, manual memory allocations. However, luckily you still have an ace up your sleeve and can show them how to implement Decorators based on value semantics (see the following guideline).

To summarize, the Decorator design pattern is one of the essential design patterns and despite some drawbacks will prove to be a very valuable addition to your toolbox. Just make sure you’re not too excited about Decorator and start to use it for everything. After all, for every pattern there is a thin line between good use and overuse.

Guideline 36: Understand the Trade-off Between Runtime and Compile Time Abstraction

In “Guideline 35: Use Decorators to Add Customization Hierarchically”, I introduced you to the Decorator design pattern and hopefully gave you a strong incentive to add this design pattern to your toolbox. However, so far I have illustrated Decorator only by means of classic, object-oriented implementations and again not followed the advice of “Guideline 22: Prefer Value Semantics over Reference Semantics. Since I assume that you are eagerly waiting to see how to implement Decorator based on value semantics, it’s time to show you two possible approaches. Yes, two approaches: I will make up for the deferral by demonstrating two very different implementations. Both are firmly based on value semantics, but in comparison, they are almost on opposite sides of the design space. While the first approach will be an implementation based on static polymorphism, which enables you to exploit all compile-time information you may have, the second approach will rather exploit all the runtime advantages of dynamic polymorphism. Both approaches have their merits but, of course, also their characteristic demerits. Therefore, these examples will nicely demonstrate the broadness of design choices available to you.

A Value-Based Compile Time Decorator

Let’s start with the Decorator implementation based on static polymorphism. “I assume that this will again be very heavy on templates, right?” you ask. Yes, I will use templates as the primary abstraction mechanism, and yes, I will use a C++20 concept and even forwarding references. But no, I will try not to make it particularly heavy on templates. On the contrary, the major focus still lies on the design aspects of the Decorator design pattern and the goal to make it easy to add new kinds of Decorators and new kinds of regular items. One such item is the ConferenceTicket class:

//---- <ConferenceTicket.h> ----------------

#include <Money.h>
#include <string>
#include <utility>

class ConferenceTicket
{
 public:
   ConferenceTicket( std::string name, Money price )
      : name_{ std::move(name) }
      , price_{ price }
   {}

   std::string const& name() const { return name_; }
   Money price() const { return price_; }

 private:
   std::string name_;
   Money price_;
};

The ConferenceTicket perfectly fulfills the expectations of a value type: there is no base class involved and there are no virtual functions. This indicates that items are no longer decorated via pointer-to-base, but instead by means of composition, or alternatively, by means of direct non-public inheritance. Two examples for this are the following implementations of the Discounted and Taxed classes:

//---- <PricedItem.h> ----------------

#include <Money.h>

template< typename T >
concept PricedItem =  3
   requires ( T item ) {
      { item.price() } -> std::same_as<Money>;
   };


//---- <Discounted.h> ----------------

#include <Money.h>
#include <PricedItem.h>
#include <utility>

template< double discount, PricedItem Item >
class Discounted  // Using composition  1
{
 public:
   template< typename... Args >
   explicit Discounted( Args&&... args )
      : item_{ std::forward<Args>(args)... }
   {}

   Money price() const {
      return item_.price() * ( 1.0 - discount );
   }

 private:
   Item item_;
};


//---- <Taxed.h> ----------------

#include <Money.h>
#include <PricedItem.h>
#include <utility>

template< double taxRate, PricedItem Item >
class Taxed : private Item  // Using inheritance  2
{
 public:
   template< typename... Args >
   explicit Taxed( Args&&... args )
      : Item{ std::forward<Args>(args)... }
   {}

   Money price() const {
      return Item::price() * ( 1.0 + taxRate );
   }
};

Both Discounted (1) and Taxed (2) serve as Decorators for other kinds of Items: the Discounted class represents a certain discount on a given item, and the Taxed class represents some kind of tax. This time, however, both are implemented in the form of class templates. The first template argument specifies the discount and the tax rate, respectively, and the second template argument specifies the type of the decorated Item.10

Most noteworthy, however, is the PricedItem constraint on the second template argument (3). This constraint represents the set of semantic requirements, i.e. the expected behavior. Due to this constraint, you can only provide types that represent items with a price() member function. Using any other type would immediately result in a compilation error. Thus PricedItem plays the same role as the Item base class in the classic Decorator implementation in “Guideline 35: Use Decorators to Add Customization Hierarchically”. For the same reason, it also represents the separation of concerns based on the Single-Responsibility Principle (SRP). Furthermore, if this constraint is owned by some high level in your architecture, then you, as well as anyone else, are able to add new kinds of items and new kinds of Decorators on any lower level. This feature perfectly fulfills the Open-Closed Principle (OCP), and due to the proper ownership of the abstraction, also the Dependency Inversion Principle (DIP) (see Figure 9-7).11

The dependency graph for the compile time _Decorator_.
Figure 9-7. Dependency graph for the compile time Decorator

Both the Discounted and Taxed class templates are very similar, except for the way they handle the decorated Item: while the Discounted class template stores the Item in the form of a data member and therefore follows “Guideline 20: Favor Composition over Inheritance”, the Taxed class template privately inherits from the given Item class. Both approaches are possible, reasonable, and have their individual strengths, but you should consider the composition approach taken by the Discounted class template as the more common approach. As explained in “Guideline 24: Use Adapters to Standardize Interfaces”, there are only five reasons to prefer non-public inheritance to composition (some of them are very rare):

  • If you have to override a virtual function

  • If you need access to a protected member function

  • If you need the adapted type to be constructed before another base class

  • If you need to share a common virtual base class or override the construction of a virtual base class

  • If you can draw significant advantage from the Empty Base Optimization (EBO)

Arguably, for a large number of adapters, EBO may be a reason to favor inheritance, but you should make sure that your choice is backed up by numbers (for instance, by means of representative benchmarks).

With these three classes in place, you’re able to specify a ConferenceTicket with a discount of 20% and a tax of 15%:

#include <ConferenceTicket.h>
#include <Discounted.h>
#include <Taxed.h>
#include <cstdlib>

int main()
{
   // 20% discount, 15% tax: (499*0.8)*1.15 = 459.08
   Taxed<0.15,Discounted<0.2,ConferenceTicket>> item{ "Core C++", 499.0 };

   Money const totalPrice = item.price();  // Results in 459.08

   // ...

   return EXIT_SUCCESS;
}

The biggest advantage of this compile-time approach is the significant performance improvement: since there are no pointer indirections, and due to the possibility of inlining, the compiler is able to go all out on optimizing the resulting code. Also, the resulting code is arguably much shorter and not bloated with any boilerplate code, and therefore easier to read.

“Could you be a little more specific about the performance results? In C++, developers are bickering about a 1% performance difference and call it significant. So seriously: how much faster is the compile-time approach?” I see, you seem familiar with the performance zeal of the C++ community. Well, as long as you promise me, again, that you won’t consider my results the definitive answer but only a single example, and if we agree that this comparison won’t evolve into a performance study, I can show you some numbers. But before I do, let me quickly outline the benchmark that I will use: I am comparing the classic object-oriented implementation from “Guideline 35: Use Decorators to Add Customization Hierarchically” with the described compile-time version. Of course, there is an arbitrary number of decorator combinations, but I am restricting myself to the following four item types:12

using DiscountedConferenceTicket = Discounted<0.2,ConferenceTicket>;
using TaxedConferenceTicket = Taxed<0.19,ConferenceTicket>;
using TaxedDiscountedConferenceTicket =
   Taxed<0.19,Discounted<0.2,ConferenceTicket>>;
using DiscountedTaxedConferenceTicket =
   Discounted<0.2,Taxed<0.19,ConferenceTicket>>;

Since in the compile time solution these four types do not have a common base class, I am filling four specific std::vectors with these. In comparison, for the classic runtime solution, I use a single std::vector of std::unique_ptr<Item>s. In total, I am creating 10,000 items with random prices for both solutions and calling std::accumulate() 5,000 times to compute the total price of all items.

With this background information, let’s take a look at the performance results (Table 9-1). Again, I am normalizing the results, this time to the performance of the runtime implementation.

Table 9-1. Performance results for the compile-time Decorator implementation (normalized performance)
GCC 11.1 Clang 11.1

Classic Decorator

1.0

1.0

Compile-time Decorator

0.078067

0.080313

As stated before, the performance of the compile-time solution is significantly faster than the runtime solution: for both GCC and Clang, it only takes approximately 8% of the time of the runtime solution, and is therefore faster by more than one order of magnitude. I know, this sounds amazing. However, while the performance of the compile-time solution is extraordinary, it comes with a couple of potentially severe limitations: due to the complete focus on templates, there is no runtime flexibility left. Since even the discount and tax rates are realized via template parameters, a new type needs to be created for each different rate. This may lead to longer compile times and more generated code (i.e., larger executables). Additionally, it stands to reason that all class templates reside in header files, which again increases compile time and may reveal more implementation details than desired. More importantly, changes to the implementation details are widely visible and may cause massive recompilations. However, the most limiting factor appears to be that the solution can only be used in this form if all information is available at compile time. Thus, you may be able to get to this performance level for only a few special cases.

A Value-Based Runtime Decorator

Since the compile time Decorator may be fast but very inflexible at runtime, let’s turn our attention to the second value-based Decorator implementation. With this implementation, we will return to the realm of dynamic polymorphism, with all of its runtime flexibility.

As you now know the Decorator design pattern, you realize that we need to be able to easily add new types: new kinds of Item, as well as new price modifiers. Therefore the design pattern of choice to turn the Decorator implementation from “Guideline 35: Use Decorators to Add Customization Hierarchically” into a value semantics–based implementation is Type Erasure.13 The following Item class implements an owning Type Erasure wrapper for our priced item example:

//---- <Item.h> ----------------

#include <Money.h>
#include <memory>
#include <utility>

class Item
{
 public:
   // ...

 private:
   struct Concept  4
   {
      virtual ~Concept() = default;
      virtual Money price() const = 0;
      virtual std::unique_ptr<Concept> clone() const = 0;
   };

   template< typename T >
   struct Model : public Concept  5
   {
      explicit Model( T const& item ) : item_( item ) {}
      explicit Model( T&& item ) : item_( std::move(item) ) {}

      Money price() const override
      {
         return item_.price();
      }

      std::unique_ptr<Concept> clone() const override
      {
         return std::make_unique<Model<T>>(*this);
      }

      T item_;
   };

   std::unique_ptr<Concept> pimpl_;
};

In this implementation, the Item class defines a nested Concept base class in its private section (4). As usual, the Concept base class represents the set of requirements (i.e. the expected behavior) for the wrapped types, which are expressed by the price() and clone() member functions. These requirements are implemented by the nested Model class template (5). Model implements the price() function by forwarding the call to the price() member function of the stored item_ data member, and the clone() function by creating a copy of the stored item.

The public section of the Item class should look familiar:

//---- <Item.h> ----------------

// ...

class Item
{
 public:
   template< typename T >
   Item( T item )  6
      : pimpl_( std::make_unique<Model<T>>( std::move(item) ) )
   {}

   Item( Item const& item ) : pimpl_( item.pimpl_->clone() ) {}

   Item& operator=( Item const& item )
   {
      pimpl_ = item.pimpl_->clone();
      return *this;
   }

   ~Item() = default;
   Item( Item&& ) = default;
   Item& operator=( Item&& item ) = default;

   Money price() const { return pimpl_->price(); }  7

 private:
   // ...
};

Next to the usual implementation of the Rule of 5, the class is again equipped with a templated constructor that accepts all kinds of items (6). Last but not least, the class provides a price() member function, which mimics the expected interface of all items (7).

With this wrapper class in place, you are able to add new items easily: neither any intrusive modification of existing code nor any use of a base class is required. Any class that provides a price() member function and is copyable will work. Luckily, this includes the ConferenceTicket class from our compile-time Decorator implementation, which provides everything we need and is firmly based on value semantics. Unfortunately, this is not true for the Discounted and Taxed classes, since they expect decorated items in the form of a template argument. Therefore, we re-implement Discounted and Taxed for use in the Type Erasure context:

//---- <Discounted.h> ----------------

#include <Item.h>
#include <utility>

class Discounted
{
 public:
   Discounted( double discount, Item item )
      : item_( std::move(item) )
      , factor_( 1.0 - discount )
   {}

   Money price() const
   {
      return item_.price() * factor_;
   }

 private:
   Item item_;
   double factor_;
};


//---- <Taxed.h> ----------------

#include <Item.h>
#include <utility>

class Taxed
{
 public:
   Taxed( double taxRate, Item item )
      : item_( std::move(item) )
      , factor_( 1.0 + taxRate )
   {}

   Money price() const
   {
      return item_.price() * factor_;
   }

 private:
   Item item_;
   double factor_;
};

It’s particularly interesting to note that neither of these two classes are derived from any base class, yet both perfectly implement the Decorator design pattern. On the one hand, they implement the operations required by the Item wrapper to count as an item (in particular, the price() member function and the copy constructor), but on the other hand, they own an Item. Therefore, both enable you to combine Decorators arbitrarily, as demonstrated in the following main() function:

#include <ConferenceTicket.h>
#include <Discounted.h>
#include <Taxed.h>

int main()
{
   // 20% discount, 15% tax: (499*0.8)*1.15 = 459.08
   Item item(Taxed(0.19, Discounted(0.2, ConferenceTicket{"Core C++",499.0})));

   Money const totalPrice = item.price();

   // ...

   return EXIT_SUCCESS;
}

“Wow, this is beautiful: there are no pointers, no manual allocations, and it feels very natural and intuitive. But at the same time, it’s extremely flexible. This is too good to be true—there must be a catch. What about the performance?” you say. Well, you sound like you expect a total performance breakdown. So let’s benchmark this solution. Of course, I’m using the same benchmark as for the compile-time version of Decorator and just adding the third solution based on Type Erasure. The performance numbers are shown in Table 9-2.

Table 9-2. Performance results for the Type Erasure Decorator implementation (normalized performance)
GCC 11.1 Clang 11.1

Classic Decorator

1.0

1.0

Compile-time Decorator

0.078067

0.080313

Type Erasure Decorator

0.997510

0.971875

As you can see, the performance is not worse than the performance of the other, classic runtime solution. In fact, the performance even appears to be a tiny bit better, but although this is an average of many runs, I wouldn’t put too much emphasis on that. However, remember that there are multiple options to improve the performance of the Type Erasure solution, as demonstrated in “Guideline 33: Be Aware of the Optimization Potential of Type Erasure”.

While performance may not be the primary strength of the runtime solution(s) (at least in comparison to a compile-time solution), it definitely shines when it comes to runtime flexibility. For instance, it is possible to decide at runtime to wrap any Item in another Decorator (based on user input, based on the result of a computation, …). This, of course, will again yield an Item, which, together with many other Items, can be stored in a single container. It indeed gives you an enormous runtime flexibility.

Another strength is the ability to hide implementation details in source files more easily. While this may result in a loss of runtime performance, it will likely result in better compile times. Most importantly: any modification to the hidden code will not affect any other code and thus save you a lot of recompilations, because the implementation details are more strongly encapsulated.

In summary, both the compile-time and runtime solutions are value based and lead to simpler, more comprehensible user code. However, they also come with individual strengths and weaknesses: while the runtime approach offers more flexibility, the compile-time approach dominates with respect to performance. In reality, you will rarely end up with a pure compile time or runtime approach, but you will very often find yourself somewhere between these two extremes. Make sure you know your options: weigh them against each other and find a compromise that perfectly combines the best of both worlds and fits your particular situation.

1 Remember “Guideline 2: Design for Change” and Core Guideline C.133: “Avoid protected data.”

2 See “Guideline 20: Favor Composition over Inheritance” for a discussion on why so many design patterns draw their power from composition rather than inheritance.

3 A null object represents an object with neutral (null) behavior. As such, it can be seen as a default for a Strategy implementation.

4 Erich Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software.

5 You may be wondering if this is the most reasonable approach for dealing with taxes. No, unfortunately it’s not. That’s because first, as usual, reality is so much more complex than this simple, educational example, and second, because in this form it’s easy to apply taxes incorrectly. While I can’t help with the first point (I’m just a mere mortal), I will go into detail about the second point at the end of this guideline.

6 If you’re wondering about the incomplete implementation: the focus here is entirely on how to design allocators, not on how to implement an allocator. For a thorough introduction on how to implement a C++17 allocator, see Nicolai Josuttis’s C++17 - The Complete Guide.

7 The metaphor of Strategy being the guts of an object and Decorator being the skin originates from the GoF book.

8 Scott Meyers, Effective C++, 3rd ed. (Addison-Wesley, 2005).

9 If you’re thinking that the original price() function should be renamed netPrice() to reflect its true purpose, then I agree.

10 Note that it is only possible to use floating-point values as non-type template parameters (NTTPs) since C++20. Alternatively, you could store the discount and tax rates in the form of data members.

11 Alternatively, in particular if you cannot use C++20 concepts yet, this is an opportunity to use the Curiously Recurring Template Pattern (CRTP); see “Guideline 26: Use CRTP to Introduce Static Type Categories”.

12 To avoid a visit from the tax collection office, I should explicitly state that I’m aware of the questionable nature of the Discounted<0.2,Taxed<0.19,ConferenceTicket>> class (see also the list of potential problems of Decorator at the end of “Guideline 35: Use Decorators to Add Customization Hierarchically”). In my defense: it’s an obvious permutation of decorators, which is well suited for this benchmark.

13 For a thorough overview of Type Erasure, see Chapter 8 and in particular “Guideline 32: Consider Replacing Inheritance Hierarchies with Type Erasure”.