top of page

CPP: Decorator

  • Writer: Ashok Kumar Kumawat
    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.


Recent Posts

See All
CPP: Lambda function

What is a Lambda Function in C++? A lambda function (often just called a "lambda") is an anonymous function—a function without a name. Introduced in C++11, lambdas allow you to write quick, throwaway

 
 
 

Comments


© 2035 by Robert Caro. Powered and secured by Wix

bottom of page