Design Patterns were always different in C++, and a usual use of these patterns in C++ might cause more complications and problems in this language. In this series of articles, I’ll show how to correctly implement them in C++. In this article, we’ll see how to implement a compile-time Decorator.
In the previous article, we saw a way to implement the Factory design pattern in C++. This time, I’ll present the way I see Decorators in C++ – at compile time.
Decorator Design Pattern
A decorator is actually an extension (or decorator) to an existing class. The classic example is an ice_cream
class:
class ice_cream {
public:
ice_cream();
private:
std::vector<ice_cream_ball> balls;
};
Assume you want to create an ice_cream
with lollipops, you can create a complete new class, with same ice_cream
properties, and you can create a decorator ice_cream
class:
class ice_cream_interface {
public:
virtual ~ice_cream_interface() = default;
virtual void add_ball() = 0;
virtual void remove_ball() = 0;
virtual void print() = 0;
};
class ice_cream : public ice_cream_interface {
public:
ice_cream();
void print() override {
std::cout << "Ice cream with " << balls.size() << " balls.\n";
}
private:
std::vector<ice_cream_ball> balls;
};
class ice_cream_with_lollipop : public ice_cream_interface {
public:
ice_cream_with_lollipop(ice_cream_interface* base_ice_cream)
: base_ice_cream(base_ice_cream) {}
void print() override {
base_ice_cream->print();
std::cout << "Lollipop type: " << lollipop_type << "\n";
}
private:
ice_cream_interface* base_ice_cream;
std::string lollipop_type;
};
Well, this is one way to implement a Decorator design pattern, but it’s a run-time decorator, and here in C++, we can use the compiler power to implement it on compile time, and to extend its functionality.
Compile Time Decorator
Let’s start with the base class:
class base {
public:
explicit base() {}
virtual ~base() = default;
virtual void func() = 0;
};
Now let’s create a minimal inherited class which every decorator should inherit from, called base_core
:
class base_core : public base {
public:
explicit base_core() {}
virtual ~base_core() = default;
void func() override {};
};
template <class T>
concept Decorator = std::is_base_of_v<base_core, T>;
Now let’s define our first class (which can also be a decorator
):
template <Decorator D = base_core>
class basic_decorator : virtual public D {
public:
void set_val(int val) { basic_decorator_val = val; }
int get_val() { return basic_decorator_val; }
void func() override {
D::func();
std::cout << "basic_decorator_val: " << basic_decorator_val << "\n";
}
private:
int basic_decorator_val;
};
Question: What if we want to decorate a class with more than one decorator
?
Assume we defined some basic decorator
s as the one above: basic_decorator_1
, basic_decorator_2
, basic_decorator_3
, basic_decorator_4
. The allocation will look something like that:
std::shared_ptr<base> b1 = std::make_shared<basic_decorator
<basic_decorator_1<basic_decorator_2<basic_decorator_3<basic_decorator_4<>>>>>>();
Looks a little bit messy and might not demonstrate a correct logic (because basic_decorator_3
not necessarily decorating basic_decorator_2
, it might decorate basic_decorator
as well). Let’s suggest a little fix:
template <Decorator ...Decorators>
class advanced_decorator : virtual public Decorators... {
public:
void set_val(int val) { advanced_decorator_val = val; }
int get_val() { return advanced_decorator_val; }
void func() override {
(Decorators::func(), ...);
std::cout << "advanced_decorator_val: " << advanced_decorator_val << "\n"
}
private:
int advanced_decorator_val;
};
But here, we got a little problem, what if we didn’t get any inheritance (a variadic template can be empty)? And still, we don’t want to force the developer to write every time the inheritance from base_core
. Here is my solution:
class empty_inheritance {};
template<typename Base = base_core, typename ...Decorators>
struct base_if_not_exists {
static constexpr bool value = sizeof...(Decorators);
using type = typename std::conditional<value, empty_inheritance, Base>::type;
};
And the usage:
template <Decorator ...Decorators>
class advanced_core : virtual public base_if_not_exists
<base_core, Decorators...>::type, virtual public Decorators... { };
Accessing fields:
void func() {
std::shared_ptr<base> my_class =
std::make_shared<advanced_core<basic_decorator_1<>, basic_decorator_2<>>>();
auto access_bd1 = dynamic_cast<basic_decorator_1<>*>(my_class.get());
auto access_bd2 = dynamic_cast<basic_decorator_2<>*>(my_class.get());
auto access_ad = dynamic_cast<advanced_core<basic_decorator_1<>,
basic_decorator_2<>>*>(my_class.get());
access_bd1->set_val(5);
access_bd2->set_val(16);
access_ad->set_val(55);
access_ad->basic_decorator_2<>::set_val(70);
my_class->func();
}
Conclusion
In C++ ‘standard’ decorators implementations might not always be the best way to use them, and sometimes it’s better to invest more time to find the best fit for their implementation.
This was a simple example for a Decorator
implementation at compile time. For a more complex example, see my repository: CppDecoratorDesignPattern.