Composite Pattern
- Description: Treat individual objects and compositions of objects uniformly through a shared interface — recursive tree structure where leaves and containers respond to the same operations.
- My Notion Note ID: K2C-2-8
- Created: 2026-05-22
- Updated: 2026-05-22
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Core Idea
- 2. Structure
- 3. C++ Example
- 4. When to Use
- 5. Variants and Pitfalls
- 6. Related Patterns
- 7. References
1. Core Idea
- Part-whole hierarchies → trees of leaves + containers.
- Clients shouldn't care if they're calling an op on a single item or a whole subtree.
- Both leaf and composite implement the same
Componentinterface → recursive uniformity.
Canonical examples → filesystem (file / directory), GUI widgets (button / panel), scene graphs (mesh / group), expression trees (literal / binary-op).
2. Structure
| Role | Responsibility |
|---|---|
| Component | Common interface for leaf and composite. Declares operations and (optionally) child management. |
| Leaf | Terminal node. Has no children. Implements operations on itself. |
| Composite | Holds children (std::vector<std::unique_ptr<Component>> typical). Forwards / aggregates operations across children. |
| Client | Manipulates the tree through Component* only. Doesn't know leaf vs composite. |
Two design choices:
- Transparent — child management (
add,remove) declared onComponent→ uniform interface; leaves either no-op or throw. - Safe — child management only on
Composite→ no nonsensical calls on leaves; clients must downcast.
GoF prefers transparent; modern C++ practice usually prefers safe, or uses std::variant.
3. C++ Example
3.1 Filesystem with shared Component
#include <memory>
#include <string>
#include <vector>
#include <iostream>
#include <numeric>
#include <utility>
class FsNode {
public:
virtual ~FsNode() = default;
virtual std::size_t size() const = 0; // bytes
virtual void print(int indent = 0) const = 0;
};
class File : public FsNode {
public:
File(std::string name, std::size_t bytes)
: name_(std::move(name)), bytes_(bytes) {}
std::size_t size() const override { return bytes_; }
void print(int indent) const override {
std::cout << std::string(indent, ' ') << name_
<< " (" << bytes_ << "B)\n";
}
private:
std::string name_;
std::size_t bytes_;
};
class Directory : public FsNode {
public:
explicit Directory(std::string name) : name_(std::move(name)) {}
void add(std::unique_ptr<FsNode> child) {
children_.push_back(std::move(child));
}
std::size_t size() const override {
return std::accumulate(children_.begin(), children_.end(), std::size_t{0},
[](std::size_t acc, const auto& c) { return acc + c->size(); });
}
void print(int indent) const override {
std::cout << std::string(indent, ' ') << name_ << "/\n";
for (const auto& c : children_) c->print(indent + 2);
}
private:
std::string name_;
std::vector<std::unique_ptr<FsNode>> children_;
};
int main() {
auto root = std::make_unique<Directory>("root");
root->add(std::make_unique<File>("readme.md", 1200));
auto src = std::make_unique<Directory>("src");
src->add(std::make_unique<File>("main.cpp", 800));
src->add(std::make_unique<File>("lib.cpp", 4000));
root->add(std::move(src));
root->print();
std::cout << "Total: " << root->size() << "B\n";
}
Client root->size() works the same whether root is a single file or a deep tree.
3.2 Modern std::variant approach
For closed sets of node kinds where virtuals feel heavy:
#include <variant>
#include <vector>
#include <memory>
struct FileV { std::string name; std::size_t bytes; };
struct DirV;
using NodeV = std::variant<FileV, DirV>;
struct DirV {
std::string name;
std::vector<std::unique_ptr<NodeV>> children;
};
std::size_t size(const NodeV& n) {
return std::visit([](const auto& x) -> std::size_t {
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, FileV>) return x.bytes;
else {
std::size_t s = 0;
for (const auto& c : x.children) s += size(*c);
return s;
}
}, n);
}
- Trade-off: no inheritance, no virtual dispatch, closed set of types → adding a new node kind touches every
visit. unique_ptr<NodeV>indirection needed becauseDirVcontains aNodeVrecursively (forward decl + heap pointer breaks the cycle).
4. When to Use
Use when:
- Recursive part-whole hierarchies (trees).
- Client code should treat single items and groups uniformly.
- New leaf / container types added often → polymorphic dispatch wins.
Avoid when:
- Structure isn't a tree (graph with cycles → needs different abstraction; risk of infinite recursion).
- One-level container only → just use
std::vector<Leaf>. - Operations on leaves and composites genuinely differ → forcing them through a shared interface yields no-op methods or runtime checks.
5. Variants and Pitfalls
- Transparent vs Safe. Transparent puts
add/removeonComponent→ leaves throw or no-op. Safe puts them only onComposite→ typesafe but loses uniformity at child-management time. - Parent pointers. Storing
parent_enables upward navigation but introduces back-reference lifetime hazards. Useweak_ptror raw non-owning pointer; owner is the parent. - Caching aggregates. Recomputing
size()walks the whole subtree every call → cache + invalidate on mutation, or accept the cost. - Iteration order. Tree traversal kind (pre/post/level) matters when ops have side effects.
- Ownership.
unique_ptrchildren → clear ownership.shared_ptr→ DAGs sneak in; recursion can loop forever. - Cycle hazard. Composite assumes a tree. Insert a node as its own ancestor → infinite recursion. Add a sanity check on
add.
6. Related Patterns
- Composite vs Decorator. Both rely on a shared base interface and recursive composition. Decorator wraps one child to add behavior; Composite holds many children to aggregate behavior. Decorator's primary effect is behavior augmentation; Composite's is structural grouping.
- Composite + Visitor. Visitor lets new operations be added without touching the component hierarchy — ideal for tree-shaped Composites with closed node sets.
- Composite + Iterator. Iterator traverses Composite in a particular order without exposing structure.
- Composite + Chain of Responsibility. Pass requests up parent links (when parent ptrs exist) until handled.
- Composite vs Flyweight. Composite leaves can be Flyweights → e.g. text glyphs shared across many paragraphs in a document tree.
7. References
- GoF — Design Patterns, Composite ch.
- refactoring.guru — Composite
- sourcemaking — Composite
- cppreference —
std::variant,std::visit