Skip to content
C++
Idiom
since C++11
Intermediate

Decorator Pattern

"C++ decorator pattern: virtual wrapping, CRTP static decorators, and std::function chaining — ownership, composition, and pitfalls."

Decorator Patternsince C++11

A structural idiom that wraps an object to add behavior layer by layer while preserving the original interface — composable at runtime through virtual dispatch or at compile time through CRTP and templates.

Overview

The Decorator pattern maps to three distinct C++ idioms with different trade-offs. Choosing correctly depends on whether composition is known at compile time, whether overhead matters, and whether you are decorating objects or callables.

ApproachCompositionDispatch overheadStored as
Virtual decoratorRuntimevtableunique_ptr chain
CRTP / templatesCompile timeNone (fully inlined)Value type
std::function chainRuntimeClosure allocationstd::function
std::streambufRuntimeMinimalRaw pointer

The virtual approach enables dynamic composition from configuration or plugins. CRTP is the choice for performance-critical wrapping where types are fixed at compile time. std::function chaining is natural for middleware and pipeline architectures.

Virtual Decorator

Each decorator holds a std::unique_ptr (C++11) to the next layer and forwards calls after augmenting behavior:

cpp
#include <memory>        // std::unique_ptr — C++11
#include <string_view>   // C++17
#include <format>        // C++20
#include <chrono>        // std::chrono::system_clock

struct Logger {
    virtual void log(std::string_view msg) = 0;  // std::string_view: C++17
    virtual ~Logger() = default;
};

struct ConsoleLogger : Logger {
    void log(std::string_view msg) override {
        std::cout << msg << '\n';
    }
};

struct TimestampLogger : Logger {
    explicit TimestampLogger(std::unique_ptr<Logger> inner)  // C++11: unique_ptr
        : inner_{std::move(inner)} {}

    void log(std::string_view msg) override {
        auto now = std::chrono::system_clock::now();
        // std::format with chrono time_point requires C++20 + <chrono> formatters
        inner_->log(std::format("[{:%F %T}] {}", now, msg));
    }

private:
    std::unique_ptr<Logger> inner_;
};

struct PrefixLogger : Logger {
    PrefixLogger(std::unique_ptr<Logger> inner, std::string prefix)
        : inner_{std::move(inner)}, prefix_{std::move(prefix)} {}

    void log(std::string_view msg) override {
        inner_->log(std::format("{}: {}", prefix_, msg));  // C++20: std::format
    }

private:
    std::unique_ptr<Logger> inner_;
    std::string prefix_;
};

// std::make_unique requires C++14
auto logger = std::make_unique<TimestampLogger>(
    std::make_unique<PrefixLogger>(
        std::make_unique<ConsoleLogger>(),
        "SERVER"
    )
);
logger->log("started");
// prints: [2026-05-23 12:00:00] SERVER: started

Each layer exclusively owns its inner via unique_ptr, so destroying the outermost decorator destroys the entire chain. The interface type (Logger*) is uniform at runtime, enabling dynamic composition from configuration, plugins, or user input.

Ownership caveat: if two decorators need to share an inner layer, use shared_ptr (C++11). Treat this as an explicit design decision — shared ownership complicates reasoning about lifetimes and is not the default.

CRTP Static Decorator

When all types are known at compile time, CRTP eliminates vtable dispatch entirely. Each decorator wraps the inner type by value:

cpp
#include <string_view>  // C++17
#include <fstream>

// NVI base — optional but gives uniform call syntax
template<typename Derived>
struct StreamBase {
    void write(std::string_view data) {
        static_cast<Derived*>(this)->write_impl(data);
    }
};

struct FileStream : StreamBase<FileStream> {
    explicit FileStream(const char* path) : file_{path} {}
    void write_impl(std::string_view data) { file_ << data; }
private:
    std::ofstream file_;
};

// CRTP decorator: compresses before forwarding
template<typename Inner>
struct CompressedStream : StreamBase<CompressedStream<Inner>> {
    explicit CompressedStream(Inner inner) : inner_{std::move(inner)} {}
    void write_impl(std::string_view data) {
        auto compressed = lz4_compress(data);  // your compression call
        inner_.write(compressed);
    }
private:
    Inner inner_;
};

// CRTP decorator: appends CRC32 after data
template<typename Inner>
struct ChecksumStream : StreamBase<ChecksumStream<Inner>> {
    explicit ChecksumStream(Inner inner) : inner_{std::move(inner)} {}
    void write_impl(std::string_view data) {
        inner_.write(data);
        uint32_t cs = crc32(data);
        inner_.write({reinterpret_cast<const char*>(&cs), sizeof(cs)});
    }
private:
    Inner inner_;
};

// C++17 CTAD deduces Inner from constructor argument
auto stream = ChecksumStream{CompressedStream{FileStream{"out.bin"}}};
stream.write("hello");  // checksums → compresses → writes; fully inlined

With C++20 concepts, constrain Inner to prevent opaque substitution errors:

cpp
// C++20
template<typename T>
concept Writable = requires(T t, std::string_view sv) {
    { t.write(sv) } -> std::same_as<void>;
};

template<Writable Inner>
struct ChecksumStream : StreamBase<ChecksumStream<Inner>> { /* ... */ };

Critical limitation: ChecksumStream<CompressedStream<FileStream>> and CompressedStream<FileStream> are unrelated concrete types. You cannot store differently-decorated variants in the same vector or pass them through a runtime interface without type erasure. When you need runtime interchangeability, wrap the CRTP chain behind a single virtual interface or std::any (C++17).

std::function Decorator Chain

For decorating callables — HTTP middleware, request pipelines, retry logic — compose std::function (C++11) closures:

cpp
#include <functional>   // std::function — C++11
#include <chrono>
#include <stdexcept>

using Handler = std::function<std::string(std::string_view)>;

// Decorator factory: wraps with structured logging
Handler with_logging(Handler h, std::string name) {
    return [h = std::move(h), name = std::move(name)](std::string_view req) {
        std::clog << '[' << name << "] → " << req << '\n';
        auto resp = h(req);
        std::clog << '[' << name << "] ← " << resp << '\n';
        return resp;
    };
}

// Decorator: measures wall time
Handler with_timing(Handler h) {
    return [h = std::move(h)](std::string_view req) {
        auto t0 = std::chrono::steady_clock::now();
        auto resp = h(req);
        auto us = std::chrono::duration_cast<std::chrono::microseconds>(
            std::chrono::steady_clock::now() - t0).count();
        std::clog << "elapsed: " << us << "µs\n";
        return resp;
    };
}

// Decorator: retries on exception, rethrows after max attempts
Handler with_retry(Handler h, int max_retries) {
    return [h = std::move(h), max_retries](std::string_view req) {
        for (int i = 0; i < max_retries; ++i) {
            try { return h(req); }
            catch (...) { if (i == max_retries - 1) throw; }
        }
        return std::string{};  // unreachable; suppresses compiler warning
    };
}

Handler base = [](std::string_view req) {
    return "response:" + std::string(req);
};

// Compose: outermost applied last, executes first
Handler handler = with_logging(with_timing(with_retry(base, 3)), "API");
handler("GET /users");

std::function incurs a heap allocation for captures that exceed the internal small-buffer optimization (typically 16–32 bytes per implementation). For high-throughput paths, benchmark against a template-based approach. C++23's std::move_only_function drops the copyability requirement, reducing overhead for move-only captures and allowing non-copyable lambdas.

std::streambuf Decorator

The standard library itself uses decorator internally. Subclass std::streambuf to filter stream I/O transparently:

cpp
#include <streambuf>
#include <ostream>
#include <cctype>   // std::toupper

class UppercaseBuf : public std::streambuf {
    std::streambuf* inner_;
protected:
    int overflow(int c) override {
        if (c == EOF) return EOF;
        return inner_->sputc(static_cast<char>(std::toupper(c)));
    }
    std::streamsize xsputn(const char* s, std::streamsize n) override {
        for (std::streamsize i = 0; i < n; ++i)
            inner_->sputc(static_cast<char>(std::toupper(s[i])));
        return n;
    }
public:
    explicit UppercaseBuf(std::streambuf* buf) : inner_{buf} {}
};

UppercaseBuf upper_buf{std::cout.rdbuf()};
std::ostream upper_out{&upper_buf};
upper_out << "hello world\n";  // prints: HELLO WORLD

This pattern underlies std::ofstream (wraps a filebuf), compression and encryption stream libraries (Boost.Iostreams), and network protocol adapters. The key property: the std::ostream interface is preserved, so existing code using operator<< requires no changes.

Best Practices

Virtual decorators:

  • Always declare virtual ~Base() = default in the interface. Omitting it causes undefined behavior when the outermost decorator is deleted through a base pointer — the most common decorator bug.
  • Delete or define copy/move on the base if decorators hold unique resources. The implicitly-generated copy may shallow-copy the unique_ptr inner and trigger a double-free.
  • Prefer unique_ptr for ownership. shared_ptr is only warranted when two decorators explicitly share an inner object.

CRTP decorators:

  • Add a C++20 concept constraint on the Inner parameter. Without it, a type mismatch produces multi-page substitution failure output — the error originates at the call site but the message points into the decorator's internals.
  • C++17 CTAD makes composition syntax clean (ChecksumStream{CompressedStream{...}}). For C++14, write explicit deduction guides or factory functions (make_checksum(make_compressed(...))).
  • Each composition level is a distinct type. Design upfront for whether you need to store mixed-decoration variants at runtime; if so, add a single virtual interface on top of the chain.

std::function chains:

  • Keep capture size within the SBO threshold (≤16 bytes is safe on most ABIs) to avoid per-call heap allocation.
  • Use std::move_only_function (C++23) when wrapped callables are move-only or when you want to express that the handler is not copyable by contract.
  • Avoid nesting more than 3–4 layers. Beyond that, each call crosses O(N) closure frames; consider a pipeline vector with a loop instead.

Common Pitfalls

Missing virtual destructor — every abstract interface needs virtual ~Interface() = default. Without it, delete outermost is UB when the static type is the base. This is silent in most compilers unless you enable -Wall.

Slicing on pass-by-value:

cpp
void process(Logger& l);  // correct — preserves dynamic type
void process(Logger l);   // UB — slices to Logger, pure-virtual call at runtime

Always pass decorated objects through pointer or reference, never by value.

CRTP: type identity breaks collections — you cannot mix ChecksumStream<FileStream> and CompressedStream<FileStream> in a vector<StreamBase*> because each is a distinct instantiation with a different Derived. Solve with type erasure (a virtual base wrapping the value-typed chain) or by committing to a single composition at compile time.

std::function overhead in hot pathsstd::function::operator() is an indirect call (equivalent to a virtual dispatch). A retry decorator layered over a timing decorator layered over a logging decorator means three indirect calls per request. Acceptable for I/O-bound work; measurable in CPU-bound tight loops. Profile before layering in performance-sensitive code.

Decorator identity and equality — a decorated object is not == to the original under default operator== semantics. If your code checks identity (e.g., registry lookup, deduplication), decorators must forward equality and hashing explicitly, or the outer wrapper must be transparent to identity checks.

Circular decoration — passing *this or its inner_ pointer as the argument to the next decorator compiles fine and crashes at call time with a stack overflow. Validate at construction that the inner pointer is not this.

See Also

  • CRTP — the compile-time polymorphism mechanism behind static decorators
  • Type Erasure — wrap a CRTP chain behind a single runtime-storable handle
  • Policy-Based Design — alternative compile-time composition without inheritance hierarchies
  • RAII — complements decorator: wraps a resource lifetime rather than a behavior