C++ Decorator Pattern Complete Guide | Dynamic Feature Addition and Composition

C++ Decorator Pattern Complete Guide | Dynamic Feature Addition and Composition

이 글의 핵심

Decorator pattern: stack behaviors around a core object—streams, middleware, and transparent feature toggles.

What is the Decorator Pattern? Why Do We Need It?

Problem Scenario: Feature Combination Explosion

Problem: To add milk, sugar, and whipped cream to coffee, you would need to create a class for every combination.

// Bad example: Class explosion
class Coffee {};
class CoffeeWithMilk : public Coffee {};
class CoffeeWithSugar : public Coffee {};
class CoffeeWithMilkAndSugar : public Coffee {};
class CoffeeWithMilkAndSugarAndWhip : public Coffee {};
// The more combinations, the more classes you need

Solution: The Decorator Pattern allows you to dynamically add features. A Decorator wraps a Component to add functionality.

// Good example: Decorator
auto coffee = std::make_unique<SimpleCoffee>();
coffee = std::make_unique<MilkDecorator>(std::move(coffee));
coffee = std::make_unique<SugarDecorator>(std::move(coffee));
// Combine features at runtime
flowchart TD
    component["Component (Coffee)"]
    simple["SimpleCoffee"]
    decorator["Decorator"]
    milk["MilkDecorator"]
    sugar["SugarDecorator"]
    
    component <|-- simple
    component <|-- decorator
    decorator <|-- milk
    decorator <|-- sugar
    decorator --> component

Table of Contents

  1. Basic Structure
  2. Stream Decorator
  3. Logging System
  4. Common Errors and Solutions
  5. Production Patterns
  6. Complete Example: Text Formatter

1. Basic Structure

#include <iostream>
#include <memory>
#include <string>

class Coffee {
public:
    virtual std::string getDescription() const = 0;
    virtual double cost() const = 0;
    virtual ~Coffee() = default;
};

class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override {
        return "Simple coffee";
    }
    
    double cost() const override {
        return 2.0;
    }
};

class CoffeeDecorator : public Coffee {
public:
    CoffeeDecorator(std::unique_ptr<Coffee> c)
        : coffee(std::move(c)) {}
    
protected:
    std::unique_ptr<Coffee> coffee;
};

class MilkDecorator : public CoffeeDecorator {
public:
    using CoffeeDecorator::CoffeeDecorator;
    
    std::string getDescription() const override {
        return coffee->getDescription() + " + Milk";
    }
    
    double cost() const override {
        return coffee->cost() + 0.5;
    }
};

class SugarDecorator : public CoffeeDecorator {
public:
    using CoffeeDecorator::CoffeeDecorator;
    
    std::string getDescription() const override {
        return coffee->getDescription() + " + Sugar";
    }
    
    double cost() const override {
        return coffee->cost() + 0.3;
    }
};

int main() {
    auto coffee = std::make_unique<SimpleCoffee>();
    std::cout << coffee->getDescription() << ": $" << coffee->cost() << '\n';
    
    coffee = std::make_unique<MilkDecorator>(std::move(coffee));
    std::cout << coffee->getDescription() << ": $" << coffee->cost() << '\n';
    
    coffee = std::make_unique<SugarDecorator>(std::move(coffee));
    std::cout << coffee->getDescription() << ": $" << coffee->cost() << '\n';
}

Output:

Simple coffee: $2
Simple coffee + Milk: $2.5
Simple coffee + Milk + Sugar: $2.8

Understanding unique_ptr Movement

How std::move works: When you write std::move(coffee), you’re transferring ownership of the object from one unique_ptr to another.

auto coffee = std::make_unique<SimpleCoffee>();  // coffee owns SimpleCoffee
coffee = std::make_unique<MilkDecorator>(std::move(coffee));  
// 1. std::move(coffee) converts coffee to rvalue
// 2. MilkDecorator constructor takes ownership
// 3. Original coffee becomes nullptr

Step-by-step execution:

// Initial state
unique_ptr<Coffee> coffee -> [SimpleCoffee object]

// After std::move(coffee)
unique_ptr<Coffee> coffee -> nullptr
MilkDecorator constructor receives -> [SimpleCoffee object]

// After assignment
unique_ptr<Coffee> coffee -> [MilkDecorator object]
                                 |
                                 v
                            [SimpleCoffee object]

Memory ownership chain:

flowchart TD
    A["coffee (unique_ptr)"] --> B["MilkDecorator"]
    B --> C["SimpleCoffee"]
    
    style A fill:#e1f5ff
    style B fill:#fff4e1
    style C fill:#f0f0f0

Important: After std::move(), the original unique_ptr becomes nullptr. Accessing it causes undefined behavior.

auto coffee = std::make_unique<SimpleCoffee>();
auto decorated = std::make_unique<MilkDecorator>(std::move(coffee));

// ❌ UB: coffee is now nullptr
coffee->cost();  // Crash!

// ✅ Use the new owner
decorated->cost();  // OK

2. Stream Decorator

#include <iostream>
#include <memory>
#include <string>
#include <algorithm>

class DataStream {
public:
    virtual void write(const std::string& data) = 0;
    virtual std::string read() = 0;
    virtual ~DataStream() = default;
};

class FileStream : public DataStream {
public:
    void write(const std::string& data) override {
        buffer = data;
        std::cout << "[File] Written: " << data << '\n';
    }
    
    std::string read() override {
        std::cout << "[File] Reading\n";
        return buffer;
    }
    
private:
    std::string buffer;
};

class StreamDecorator : public DataStream {
public:
    StreamDecorator(std::unique_ptr<DataStream> s)
        : stream(std::move(s)) {}
    
protected:
    std::unique_ptr<DataStream> stream;
};

class EncryptionDecorator : public StreamDecorator {
public:
    using StreamDecorator::StreamDecorator;
    
    void write(const std::string& data) override {
        std::string encrypted = encrypt(data);
        std::cout << "[Encryption] Encrypting\n";
        stream->write(encrypted);
    }
    
    std::string read() override {
        std::string encrypted = stream->read();
        std::cout << "[Encryption] Decrypting\n";
        return decrypt(encrypted);
    }
    
private:
    std::string encrypt(const std::string& data) {
        std::string result = data;
        std::reverse(result.begin(), result.end());
        return result;
    }
    
    std::string decrypt(const std::string& data) {
        return encrypt(data);  // Symmetric
    }
};

class CompressionDecorator : public StreamDecorator {
public:
    using StreamDecorator::StreamDecorator;
    
    void write(const std::string& data) override {
        std::string compressed = compress(data);
        std::cout << "[Compression] Compressing\n";
        stream->write(compressed);
    }
    
    std::string read() override {
        std::string compressed = stream->read();
        std::cout << "[Compression] Decompressing\n";
        return decompress(compressed);
    }
    
private:
    std::string compress(const std::string& data) {
        return "[COMPRESSED]" + data;
    }
    
    std::string decompress(const std::string& data) {
        return data.substr(12);  // Remove "[COMPRESSED]"
    }
};

int main() {
    auto stream = std::make_unique<FileStream>();
    stream = std::make_unique<EncryptionDecorator>(std::move(stream));
    stream = std::make_unique<CompressionDecorator>(std::move(stream));
    
    stream->write("Hello, World!");
    std::string data = stream->read();
    std::cout << "Result: " << data << '\n';
}

3. Logging System

#include <iostream>
#include <memory>
#include <chrono>
#include <iomanip>

class Logger {
public:
    virtual void log(const std::string& message) = 0;
    virtual ~Logger() = default;
};

class ConsoleLogger : public Logger {
public:
    void log(const std::string& message) override {
        std::cout << message << '\n';
    }
};

class LoggerDecorator : public Logger {
public:
    LoggerDecorator(std::unique_ptr<Logger> l)
        : logger(std::move(l)) {}
    
protected:
    std::unique_ptr<Logger> logger;
};

class TimestampDecorator : public LoggerDecorator {
public:
    using LoggerDecorator::LoggerDecorator;
    
    void log(const std::string& message) override {
        auto now = std::chrono::system_clock::now();
        auto time = std::chrono::system_clock::to_time_t(now);
        std::cout << "[" << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S") << "] ";
        logger->log(message);
    }
};

class LevelDecorator : public LoggerDecorator {
public:
    LevelDecorator(std::unique_ptr<Logger> l, const std::string& level)
        : LoggerDecorator(std::move(l)), level_(level) {}
    
    void log(const std::string& message) override {
        logger->log("[" + level_ + "] " + message);
    }
    
private:
    std::string level_;
};

int main() {
    auto logger = std::make_unique<ConsoleLogger>();
    logger = std::make_unique<TimestampDecorator>(std::move(logger));
    logger = std::make_unique<LevelDecorator>(std::move(logger), "INFO");
    
    logger->log("Application started");
}

4. Common Errors and Solutions

Issue 1: Type Loss

Symptom: Wrapping with a Decorator causes loss of original type information.

// ❌ Incorrect usage
SimpleCoffee* simple = new SimpleCoffee();
Coffee* decorated = new MilkDecorator(simple);
// Cannot call specific methods of simple

// ✅ Solution: Use dynamic_cast if needed
if (auto* simple = dynamic_cast<SimpleCoffee*>(decorated)) {
    simple->specificMethod();
}

Issue 2: Order Dependency

Symptom: Results vary depending on the order of Decorators.

// Encryption -> Compression vs Compression -> Encryption
// Results may differ

Issue 3: Memory Management

Symptom: Forgetting to use std::move() causes compilation errors.

// ❌ Copy attempt (unique_ptr is not copyable)
auto coffee = std::make_unique<SimpleCoffee>();
auto decorated = std::make_unique<MilkDecorator>(coffee);  // Error!

// ✅ Use std::move to transfer ownership
auto decorated = std::make_unique<MilkDecorator>(std::move(coffee));

Why unique_ptr?: unique_ptr ensures:

  1. Single ownership: Only one owner at a time
  2. Automatic cleanup: Destructor chain is called automatically
  3. No memory leaks: RAII guarantees cleanup even with exceptions

Destructor chain:

{
    auto coffee = std::make_unique<SimpleCoffee>();
    coffee = std::make_unique<MilkDecorator>(std::move(coffee));
    coffee = std::make_unique<SugarDecorator>(std::move(coffee));
}  // Automatic cleanup in reverse order:
   // 1. ~SugarDecorator()
   // 2. ~MilkDecorator()
   // 3. ~SimpleCoffee()

Manual memory management (not recommended):

// ❌ Raw pointers (error-prone)
Coffee* coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);  // Who owns the original?
delete coffee;  // Does this delete both?

// ✅ unique_ptr (safe)
auto coffee = std::make_unique<SimpleCoffee>();
coffee = std::make_unique<MilkDecorator>(std::move(coffee));
// Automatic cleanup, no leaks

Issue 4: Use After Move

auto coffee = std::make_unique<SimpleCoffee>();
auto decorated = std::make_unique<MilkDecorator>(std::move(coffee));

// ❌ Use after move
std::cout << coffee->cost();  // UB! coffee is nullptr

// ✅ Use the new owner
std::cout << decorated->cost();  // OK

5. Production Patterns

Pattern 1: Builder Style

class CoffeeBuilder {
    std::unique_ptr<Coffee> coffee;
public:
    CoffeeBuilder() : coffee(std::make_unique<SimpleCoffee>()) {}
    
    CoffeeBuilder& addMilk() {
        coffee = std::make_unique<MilkDecorator>(std::move(coffee));
        return *this;
    }
    
    CoffeeBuilder& addSugar() {
        coffee = std::make_unique<SugarDecorator>(std::move(coffee));
        return *this;
    }
    
    std::unique_ptr<Coffee> build() {
        return std::move(coffee);
    }
};

auto coffee = CoffeeBuilder()
    .addMilk()
    .addSugar()
    .build();

6. Complete Example: Text Formatter

#include <iostream>
#include <memory>
#include <string>
#include <algorithm>

class TextFormatter {
public:
    virtual std::string format(const std::string& text) = 0;
    virtual ~TextFormatter() = default;
};

class PlainTextFormatter : public TextFormatter {
public:
    std::string format(const std::string& text) override {
        return text;
    }
};

class FormatterDecorator : public TextFormatter {
public:
    FormatterDecorator(std::unique_ptr<TextFormatter> f)
        : formatter(std::move(f)) {}
    
protected:
    std::unique_ptr<TextFormatter> formatter;
};

class BoldDecorator : public FormatterDecorator {
public:
    using FormatterDecorator::FormatterDecorator;
    
    std::string format(const std::string& text) override {
        return "<b>" + formatter->format(text) + "</b>";
    }
};

class ItalicDecorator : public FormatterDecorator {
public:
    using FormatterDecorator::FormatterDecorator;
    
    std::string format(const std::string& text) override {
        return "<i>" + formatter->format(text) + "</i>";
    }
};

class UpperCaseDecorator : public FormatterDecorator {
public:
    using FormatterDecorator::FormatterDecorator;
    
    std::string format(const std::string& text) override {
        std::string result = formatter->format(text);
        std::transform(result.begin(), result.end(), result.begin(), ::toupper);
        return result;
    }
};

int main() {
    auto formatter = std::make_unique<PlainTextFormatter>();
    formatter = std::make_unique<BoldDecorator>(std::move(formatter));
    formatter = std::make_unique<ItalicDecorator>(std::move(formatter));
    formatter = std::make_unique<UpperCaseDecorator>(std::move(formatter));
    
    std::cout << formatter->format("Hello, World!") << '\n';
    // <I><B>HELLO, WORLD!</B></I>
}

Pattern 2: Decorator with shared_ptr

// When multiple decorators need to share the same component
class SharedDecorator {
    std::shared_ptr<Coffee> coffee;
public:
    SharedDecorator(std::shared_ptr<Coffee> c) : coffee(c) {}
    
    std::shared_ptr<Coffee> getWrapped() {
        return coffee;  // Can share ownership
    }
};

Pattern 3: Decorator Factory

class DecoratorFactory {
public:
    static std::unique_ptr<Coffee> create(const std::string& recipe) {
        auto coffee = std::make_unique<SimpleCoffee>();
        
        if (recipe.find("milk") != std::string::npos) {
            coffee = std::make_unique<MilkDecorator>(std::move(coffee));
        }
        if (recipe.find("sugar") != std::string::npos) {
            coffee = std::make_unique<SugarDecorator>(std::move(coffee));
        }
        
        return coffee;
    }
};

// Usage
auto coffee = DecoratorFactory::create("milk sugar");

Summary

ConceptDescription
Decorator PatternDynamically add features
PurposeExtend functionality without inheritance
StructureComponent, ConcreteComponent, Decorator
MemoryUse unique_ptr for ownership, std::move() for transfer
AdvantagesFlexible combinations, adheres to OCP, runtime addition
DisadvantagesType loss, order dependency, increased complexity
Use CasesStreams, logging, UI, text formatting

The Decorator Pattern is a powerful design pattern for dynamically combining features.


FAQ

Q1: When should I use the Decorator Pattern?

A: Use it when you need to dynamically add features and there are too many combinations to handle with inheritance.

Q2: Inheritance vs Decorator?

A: Inheritance is static, while Decorator allows dynamic composition.

Q3: How is it different from Adapter?

A: Adapter focuses on interface conversion, while Decorator focuses on adding functionality.

Q4: What about performance overhead?

A: A long chain of Decorators can increase indirect references. Each decorator adds one virtual function call. For performance-critical code, consider:

  • Limiting decorator depth
  • Using CRTP for compile-time decoration
  • Profiling to identify bottlenecks

Q5: How do I handle type loss?

A: Use dynamic_cast to restore the original type, or consider using the Visitor Pattern. Alternatively, design your interface to expose all needed functionality.

Q6: Why use unique_ptr instead of raw pointers?

A: unique_ptr provides:

  • Automatic memory management: No manual delete needed
  • Exception safety: Cleanup guaranteed even with exceptions
  • Clear ownership: Single owner, transfer with std::move()
  • Zero overhead: Same performance as raw pointers

Q7: Can I use shared_ptr instead of unique_ptr?

A: Yes, if you need shared ownership. However, unique_ptr is preferred for decorators because:

  • Decorators typically have single ownership
  • unique_ptr is more efficient (no reference counting)
  • Ownership transfer is explicit with std::move()

Q8: What happens if I forget std::move()?

A: Compilation error. unique_ptr is not copyable, only movable. This prevents accidental ownership duplication.

auto coffee = std::make_unique<SimpleCoffee>();
auto decorated = std::make_unique<MilkDecorator>(coffee);  // Error!
// error: call to deleted constructor of 'std::unique_ptr<Coffee>'

Q9: How do I debug decorator chains?

A: Add logging to each decorator’s constructor and methods to trace the execution flow:

class DebugDecorator : public CoffeeDecorator {
public:
    DebugDecorator(std::unique_ptr<Coffee> c) 
        : CoffeeDecorator(std::move(c)) {
        std::cout << "[Debug] DebugDecorator created\n";
    }
    
    std::string getDescription() const override {
        std::cout << "[Debug] getDescription called\n";
        return coffee->getDescription();
    }
};

Q10: Any resources to learn the Decorator Pattern?

A:

Related posts: Adapter Pattern, Proxy Pattern, Composition.

One-line summary: The Decorator Pattern allows you to dynamically combine features using composition and unique_ptr for safe memory management.

## 같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

- [C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성](/blog/cpp-adapter-pattern/)
- [C++ Proxy Pattern 완벽 가이드 | 접근 제어와 지연 로딩](/blog/cpp-proxy-pattern/)

---

---

## 이 글에서 다루는 키워드 (관련 검색어)

C++, decorator, pattern, wrapper, composition, inheritance 등으로 검색하시면 이 글이 도움이 됩니다.