CPP: Decorator
- Ashok Kumar Kumawat
- May 11
- 4 min read
The Decorator Pattern
The Decorator is a Structural design pattern. It allows you to dynamically attach new behaviors or responsibilities to an object by placing it inside a special wrapper object that contains these new behaviors.
The Problem It Solves
Imagine you are writing a system for a coffee shop. You start with a base Coffee class. Then, customers want variations: Coffee with Milk, Coffee with Sugar, Coffee with Vanilla, Coffee with Milk AND Sugar...
If you use standard inheritance to solve this, you get a "class explosion":
Coffee
MilkCoffee
SugarCoffee
MilkAndSugarCoffee
VanillaMilkCoffee
...and so on. Every new ingredient doubles the number of classes.
The Decorator pattern solves this by using composition instead of inheritance. You create a base object (Coffee) and "wrap" it with decorators (Milk, Sugar) as needed.
In C++, there are two primary ways to implement this pattern: Dynamic Decorators (the classic way) and Static Decorators (the modern template way).
1. Dynamic Decorator (The Classic Approach)
The Dynamic Decorator is the classic "Gang of Four" (GoF) implementation. It uses interfaces (abstract base classes) and polymorphism to wrap objects at runtime.
How it Works
Both the base object and the decorators inherit from the same interface. The decorator also holds a pointer or reference to that interface. When you call a method on the decorator, it does its own work and then forwards the request to the wrapped object.
C++ Implementation
C++
#include <iostream>
#include <string>
#include <memory>
// 1. The common interface
class Beverage {
public:
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
virtual ~Beverage() = default;
};
// 2. The core object being decorated
class Espresso : public Beverage {
public:
std::string getDescription() const override { return "Espresso"; }
double getCost() const override { return 2.00; }
};
// 3. The Base Decorator
class BeverageDecorator : public Beverage {
protected:
std::unique_ptr<Beverage> beverage; // Holds the wrapped object
public:
BeverageDecorator(std::unique_ptr<Beverage> bev) : beverage(std::move(bev)) {}
};
// 4. Concrete Decorators
class Milk : public BeverageDecorator {
public:
Milk(std::unique_ptr<Beverage> bev) : BeverageDecorator(std::move(bev)) {}
std::string getDescription() const override {
return beverage->getDescription() + ", Milk";
}
double getCost() const override {
return beverage->getCost() + 0.50; // Add 50 cents
}
};
class Sugar : public BeverageDecorator {
public:
Sugar(std::unique_ptr<Beverage> bev) : BeverageDecorator(std::move(bev)) {}
std::string getDescription() const override {
return beverage->getDescription() + ", Sugar";
}
double getCost() const override {
return beverage->getCost() + 0.20; // Add 20 cents
}
};
int main() {
// We compose the object at RUNTIME
std::unique_ptr<Beverage> myDrink = std::make_unique<Espresso>();
myDrink = std::make_unique<Milk>(std::move(myDrink));
myDrink = std::make_unique<Sugar>(std::move(myDrink));
std::cout << "Order: " << myDrink->getDescription() << std::endl;
std::cout << "Cost: $" << myDrink->getCost() << std::endl;
return 0;
}
Pros:
Runtime Flexibility: You can decide exactly which decorators to apply based on user input while the program is running.
Dynamic Reconfiguration: You can strip away or add new wrappers during the object's lifetime.
Cons:
Performance Overhead: Relies heavily on virtual function calls and pointer indirection, which can be slow in performance-critical code.
Memory Allocation: Usually requires dynamic memory allocation (e.g., std::unique_ptr).
2. Static Decorator (The Modern C++ Approach)
The Static Decorator (sometimes called a Mixin) uses C++ templates to wrap objects at compile-time.
How it Works
Instead of inheriting from a common base class, a decorator is a template class that inherits from its template parameter. The compiler figures out the exact final type and generates highly optimized code.
C++ Implementation
C++
#include <iostream>
#include <string>
// 1. Core Object (No need for a virtual base class!)
struct Espresso {
std::string getDescription() const { return "Espresso"; }
double getCost() const { return 2.00; }
};
// 2. Concrete Decorators using Templates
template <typename T>
struct Milk : public T { // Inherits from the template parameter
std::string getDescription() const {
return T::getDescription() + ", Milk";
}
double getCost() const {
return T::getCost() + 0.50;
}
};
template <typename T>
struct Sugar : public T {
std::string getDescription() const {
return T::getDescription() + ", Sugar";
}
double getCost() const {
return T::getCost() + 0.20;
}
};
int main() {
// We compose the object at COMPILE-TIME
// The type is explicitly defined in the code
Sugar<Milk<Espresso>> myDrink;
std::cout << "Order: " << myDrink.getDescription() << std::endl;
std::cout << "Cost: $" << myDrink.getCost() << std::endl;
return 0;
}
Pros:
Zero Overhead: There are no virtual functions, no pointers, and no dynamic memory allocation. The compiler can inline everything, making it blazingly fast.
Simpler Core Classes: The core class (Espresso) doesn't need to know anything about interfaces or virtual destructors.
Cons:
Rigid at Runtime: The exact combination of decorators must be known when you write the code. You cannot decide to add Sugar based on a user's button click at runtime unless you write a specific if/else statement that instantiates that exact type.
Complex Types: The resulting type can become a massive, unreadable template string (e.g., Sugar<Milk<Vanilla<Espresso>>>), which can make compiler errors difficult to read and APIs hard to design.
Summary Comparison
Feature | Dynamic Decorator (Classic OOP) | Static Decorator (Templates) |
When is it built? | Runtime | Compile-time |
Mechanism | Interfaces (virtual) & Pointers | Templates (template <typename T>) |
Performance | Slower (Virtual dispatch overhead) | Fastest (Zero overhead, easily inlined) |
Flexibility | High (Change wrappers on the fly) | Low (Fixed once compiled) |
Use Case | UI elements, streaming data, user-configurable objects. | High-performance math, game engines, embedded systems. |
Comments