C++ Composite Pattern Complete Guide | Handling Tree Structures with a Unified Interface

C++ Composite Pattern Complete Guide | Handling Tree Structures with a Unified Interface

이 글의 핵심

Composite pattern: uniform Component interface for leaves and containers—scene graphs, UI trees, and recursive algorithms.

What is the Composite Pattern? Why is it needed?

Problem Scenario: Handling Individual Objects and Groups Differently

Problem: Handling files and folders differently makes code complex.

// Bad design: Type checking required
void printSize(FileSystemItem* item) {
    if (auto* file = dynamic_cast<File*>(item)) {
        std::cout << file->getSize() << '\n';
    } else if (auto* folder = dynamic_cast<Folder*>(item)) {
        for (auto& child : folder->getChildren()) {
            printSize(child);  // Recursion
        }
    }
}

Solution: The Composite Pattern treats leaves (files) and composites (folders) with the same interface.

// Good design: Composite
class Component {
public:
    virtual int getSize() const = 0;  // Unified interface
};

class File : public Component {
    int size_;
public:
    int getSize() const override { return size_; }
};

class Folder : public Component {
    std::vector<std::shared_ptr<Component>> children_;
public:
    int getSize() const override {
        int total = 0;
        for (const auto& child : children_)
            total += child->getSize();  // Recursion
        return total;
    }
};
flowchart TD
    client["Client"]
    component["Component<br/>(getSize)"]
    leaf["Leaf<br/>(File)"]
    composite["Composite<br/>(Folder)"]
    
    client --> component
    leaf -.implements.-> component
    composite -.implements.-> component
    composite --> component

Table of Contents

  1. Basic Structure
  2. File System Example
  3. UI Component Hierarchy
  4. Common Errors and Solutions
  5. Production Patterns
  6. Complete Example: Organization Chart System

1. Basic Structure

#include <vector>
#include <memory>
#include <iostream>

class Component {
public:
    virtual void operation() const = 0;
    virtual void add(std::shared_ptr<Component>) {}
    virtual void remove(std::shared_ptr<Component>) {}
    virtual ~Component() = default;
};

// Leaf: No children
class Leaf : public Component {
    int id_;
public:
    explicit Leaf(int id) : id_(id) {}
    void operation() const override {
        std::cout << "Leaf " << id_ << '\n';
    }
};

// Composite: Holds list of children
class Composite : public Component {
    std::vector<std::shared_ptr<Component>> children_;
public:
    void add(std::shared_ptr<Component> c) override {
        children_.push_back(std::move(c));
    }
    void remove(std::shared_ptr<Component> c) override {
        children_.erase(
            std::remove(children_.begin(), children_.end(), c),
            children_.end()
        );
    }
    void operation() const override {
        std::cout << "Composite [\n";
        for (const auto& c : children_)
            c->operation();
        std::cout << "]\n";
    }
};

int main() {
    auto root = std::make_shared<Composite>();
    root->add(std::make_shared<Leaf>(1));
    
    auto branch = std::make_shared<Composite>();
    branch->add(std::make_shared<Leaf>(2));
    branch->add(std::make_shared<Leaf>(3));
    root->add(branch);
    
    root->operation();
    // Output:
    // Composite [
    // Leaf 1
    // Composite [
    // Leaf 2
    // Leaf 3
    // ]
    // ]
    return 0;
}

2. File System Example

Calculating File and Folder Sizes

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

class FileSystemItem {
public:
    virtual int getSize() const = 0;
    virtual void print(int indent = 0) const = 0;
    virtual ~FileSystemItem() = default;
};

class File : public FileSystemItem {
    std::string name_;
    int size_;
public:
    File(std::string name, int size) : name_(std::move(name)), size_(size) {}
    
    int getSize() const override { return size_; }
    
    void print(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << "File: " << name_ 
                  << " (" << size_ << " bytes)\n";
    }
};

class Folder : public FileSystemItem {
    std::string name_;
    std::vector<std::shared_ptr<FileSystemItem>> children_;
public:
    explicit Folder(std::string name) : name_(std::move(name)) {}
    
    void add(std::shared_ptr<FileSystemItem> item) {
        children_.push_back(std::move(item));
    }
    
    int getSize() const override {
        int total = 0;
        for (const auto& child : children_)
            total += child->getSize();
        return total;
    }
    
    void print(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << "Folder: " << name_ 
                  << " (" << getSize() << " bytes total)\n";
        for (const auto& child : children_)
            child->print(indent + 2);
    }
};

int main() {
    auto root = std::make_shared<Folder>("root");
    root->add(std::make_shared<File>("readme.txt", 100));
    
    auto src = std::make_shared<Folder>("src");
    src->add(std::make_shared<File>("main.cpp", 500));
    src->add(std::make_shared<File>("utils.cpp", 300));
    root->add(src);
    
    auto docs = std::make_shared<Folder>("docs");
    docs->add(std::make_shared<File>("manual.pdf", 2000));
    root->add(docs);
    
    root->print();
    // Output:
    // Folder: root (2900 bytes total)
    //   File: readme.txt (100 bytes)
    //   Folder: src (800 bytes total)
    //     File: main.cpp (500 bytes)
    //     File: utils.cpp (300 bytes)
    //   Folder: docs (2000 bytes total)
    //     File: manual.pdf (2000 bytes)
    
    return 0;
}

Key Point: getSize() is called recursively to calculate the total size of the entire tree.


3. UI Component Hierarchy

GUI Widget Tree

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

class Widget {
public:
    virtual void render() const = 0;
    virtual void add(std::shared_ptr<Widget>) {}
    virtual ~Widget() = default;
};

class Button : public Widget {
    std::string label_;
public:
    explicit Button(std::string label) : label_(std::move(label)) {}
    
    void render() const override {
        std::cout << "[Button: " << label_ << "]\n";
    }
};

class Label : public Widget {
    std::string text_;
public:
    explicit Label(std::string text) : text_(std::move(text)) {}
    
    void render() const override {
        std::cout << "Label: " << text_ << '\n';
    }
};

class Panel : public Widget {
    std::string title_;
    std::vector<std::shared_ptr<Widget>> children_;
public:
    explicit Panel(std::string title) : title_(std::move(title)) {}
    
    void add(std::shared_ptr<Widget> widget) override {
        children_.push_back(std::move(widget));
    }
    
    void render() const override {
        std::cout << "=== Panel: " << title_ << " ===\n";
        for (const auto& child : children_)
            child->render();
        std::cout << "===================\n";
    }
};

int main() {
    auto mainPanel = std::make_shared<Panel>("Main Window");
    mainPanel->add(std::make_shared<Label>("Welcome!"));
    
    auto buttonPanel = std::make_shared<Panel>("Actions");
    buttonPanel->add(std::make_shared<Button>("OK"));
    buttonPanel->add(std::make_shared<Button>("Cancel"));
    mainPanel->add(buttonPanel);
    
    mainPanel->render();
    // Output:
    // === Panel: Main Window ===
    // Label: Welcome!
    // === Panel: Actions ===
    // [Button: OK]
    // [Button: Cancel]
    // ===================
    // ===================
    
    return 0;
}

Key Point: Panel can contain other Panels or Buttons, representing nested UI hierarchies.


4. Common Errors and Solutions

Error 1: Calling add() on Leaf

// ❌ Bad: Calling add() on Leaf is silently ignored
auto file = std::make_shared<File>("test.txt", 100);
file->add(anotherFile);  // Nothing happens

Solution: Throw an exception in Leaf’s add() or add type checking.

// ✅ Good: Throw exception
class File : public FileSystemItem {
public:
    void add(std::shared_ptr<FileSystemItem>) override {
        throw std::logic_error("Cannot add to a file");
    }
};

Error 2: Circular Reference

// ❌ Bad: Circular reference
auto folder1 = std::make_shared<Folder>("A");
auto folder2 = std::make_shared<Folder>("B");
folder1->add(folder2);
folder2->add(folder1);  // Circular!

Solution: Add parent pointer to detect cycles or use weak_ptr.

// ✅ Good: Parent checking
class Folder : public FileSystemItem {
    std::weak_ptr<Folder> parent_;
public:
    void add(std::shared_ptr<FileSystemItem> item) {
        // Cycle detection logic
        children_.push_back(std::move(item));
    }
};

Error 3: Memory Leak

// ❌ Bad: Using raw pointers
class Composite {
    std::vector<Component*> children_;  // Leak risk
};

Solution: Use std::shared_ptr or std::unique_ptr.

// ✅ Good
class Composite {
    std::vector<std::shared_ptr<Component>> children_;
};

5. Production Patterns

Pattern 1: Combined with Visitor

class Visitor {
public:
    virtual void visitFile(File* file) = 0;
    virtual void visitFolder(Folder* folder) = 0;
};

class SizeCalculator : public Visitor {
    int total_ = 0;
public:
    void visitFile(File* file) override { total_ += file->getSize(); }
    void visitFolder(Folder* folder) override {
        for (auto& child : folder->getChildren())
            child->accept(this);
    }
    int getTotal() const { return total_; }
};

Pattern 2: Combined with Iterator

class Composite {
    std::vector<std::shared_ptr<Component>> children_;
public:
    auto begin() { return children_.begin(); }
    auto end() { return children_.end(); }
};

// Usage
for (auto& child : composite) {
    child->operation();
}

6. Complete Example: Organization Chart System

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

class Employee {
public:
    virtual void showDetails(int indent = 0) const = 0;
    virtual int getSalary() const = 0;
    virtual void add(std::shared_ptr<Employee>) {}
    virtual ~Employee() = default;
};

class Developer : public Employee {
    std::string name_;
    int salary_;
public:
    Developer(std::string name, int salary) 
        : name_(std::move(name)), salary_(salary) {}
    
    void showDetails(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << "Developer: " << name_ 
                  << " ($" << salary_ << ")\n";
    }
    
    int getSalary() const override { return salary_; }
};

class Manager : public Employee {
    std::string name_;
    int salary_;
    std::vector<std::shared_ptr<Employee>> team_;
public:
    Manager(std::string name, int salary) 
        : name_(std::move(name)), salary_(salary) {}
    
    void add(std::shared_ptr<Employee> emp) override {
        team_.push_back(std::move(emp));
    }
    
    void showDetails(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << "Manager: " << name_ 
                  << " ($" << salary_ << ") - Team size: " << team_.size() << '\n';
        for (const auto& emp : team_)
            emp->showDetails(indent + 2);
    }
    
    int getSalary() const override {
        int total = salary_;
        for (const auto& emp : team_)
            total += emp->getSalary();
        return total;
    }
};

int main() {
    auto ceo = std::make_shared<Manager>("Alice", 150000);
    
    auto engManager = std::make_shared<Manager>("Bob", 120000);
    engManager->add(std::make_shared<Developer>("Charlie", 80000));
    engManager->add(std::make_shared<Developer>("David", 85000));
    ceo->add(engManager);
    
    auto salesManager = std::make_shared<Manager>("Eve", 110000);
    salesManager->add(std::make_shared<Developer>("Frank", 70000));
    ceo->add(salesManager);
    
    ceo->showDetails();
    std::cout << "\nTotal company payroll: $" << ceo->getSalary() << '\n';
    
    // Output:
    // Manager: Alice ($150000) - Team size: 2
    //   Manager: Bob ($120000) - Team size: 2
    //     Developer: Charlie ($80000)
    //     Developer: David ($85000)
    //   Manager: Eve ($110000) - Team size: 1
    //     Developer: Frank ($70000)
    //
    // Total company payroll: $615000
    
    return 0;
}

Summary

ItemDescription
PurposeRecursively handle leaves and composites with the same interface
AdvantagesManage tree structures with consistent API, easily add new node types, simplify client code
DisadvantagesExpose meaningless methods like add to leaves, potential type safety weakening
When to UseFile systems, UI hierarchies, organization charts, menu structures, and other tree-like data

Related Posts: Adapter Pattern, Decorator Pattern, Iterator Guide, Visitor Pattern, Flyweight Pattern.

One-line Summary: The Composite Pattern enables you to handle tree structures like folder/file, menu hierarchies, and organization charts recursively with a unified interface.

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

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

  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ Decorator Pattern 완벽 가이드 | 기능 동적 추가와 조합
  • C++ 반복자 | “Iterator” 완벽 가이드
  • C++ Visitor Pattern | “방문자 패턴” 가이드


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

C++, Composite, design pattern, structural, tree, hierarchy, recursive 등으로 검색하시면 이 글이 도움이 됩니다.