C++ Flyweight Pattern Complete Guide | Save Memory Through Sharing

C++ Flyweight Pattern Complete Guide | Save Memory Through Sharing

이 글의 핵심

C++ Flyweight Pattern shares common state (intrinsic) and keeps individual state (extrinsic) separate to reduce memory when many objects exist, practical examples, text rendering, game tiles.

What is Flyweight Pattern? Why Needed?

Structural pattern related to sharing and caching is organized in Structural Pattern Series together with Composite and Proxy.

Problem Scenario: Memory Waste

Problem: When drawing 10,000 trees, copying texture for each tree causes memory explosion. Below is an implementation example using C++. Define a class to encapsulate data and functionality. Try running the code directly to check its operation.

// Bad design: Memory waste
class Tree {
    Texture texture_;  // 10MB
    int x_, y_;        // Only position differs
};
std::vector<Tree> forest(10000);  // 10MB × 10000 = 100GB!

Solution: Flyweight Pattern shares common state (texture) and keeps individual state (position) separate. Below is an implementation example using C++. Define a class to encapsulate data and functionality. Understand the role of each part while examining the code.

// Good design: Flyweight
// Type definition
class TreeType {  // Shared intrinsic state
    Texture texture_;  // 10MB (only 1)
};
class Tree {  // extrinsic state
    TreeType* type_;  // Pointer only (8 bytes)
    int x_, y_;       // Position
};
std::vector<Tree> forest(10000);  // 10MB + (16 bytes × 10000) = 10.16MB

Below is an implementation example using mermaid. Understand the role of each part while examining the code.

flowchart LR
    client[Client]
    factory[FlyweightFactory]
    flyweight[Flyweight
(TreeType)] context[Context
(Tree)] client --> factory factory --> flyweight context --> flyweight client --> context

1. Basic Structure

#include <unordered_map>
#include <string>
#include <memory>
#include <iostream>
// Shared internal state (intrinsic)
struct Glyph {
    char ch;
    int width, height;
    std::string bitmap;  // Actually large data
    
    Glyph(char c, int w, int h) : ch(c), width(w), height(h) {
        bitmap = std::string(w * h, '#');  // Virtual bitmap
        std::cout << "Creating Glyph '" << ch << "'\n";
    }
};
class GlyphFactory {
    std::unordered_map<char, std::shared_ptr<Glyph>> cache_;
public:
    std::shared_ptr<Glyph> get(char c) {
        auto it = cache_.find(c);
        if (it != cache_.end()) {
            std::cout << "Reusing Glyph '" << c << "'\n";
            return it->second;
        }
        auto g = std::make_shared<Glyph>(c, 8, 16);
        cache_[c] = g;
        return g;
    }
    
    size_t getCacheSize() const { return cache_.size(); }
};
// External state (extrinsic): position etc — passed at call time
void draw(std::shared_ptr<Glyph> g, int x, int y) {
    std::cout << "Draw '" << g->ch << "' at (" << x << "," << y << ")\n";
}
int main() {
    GlyphFactory factory;
    
    std::string text = "HELLO WORLD";
    int x = 0;
    for (char c : text) {
        if (c == ' ') { x += 8; continue; }
        auto glyph = factory.get(c);
        draw(glyph, x, 0);
        x += 8;
    }
    
    std::cout << "\nTotal unique glyphs: " << factory.getCacheSize() << '\n';
    // Output:
    // Creating Glyph 'H'
    // Draw 'H' at (0,0)
    // Creating Glyph 'E'
    // Draw 'E' at (8,0)
    // Reusing Glyph 'L'
    // Draw 'L' at (16,0)
    // Reusing Glyph 'L'
    // Draw 'L' at (24,0)
    // ...
    // Total unique glyphs: 7
    
    return 0;
}

2. Text Rendering Example

Font Flyweight

#include <unordered_map>
#include <memory>
#include <iostream>
#include <string>
// Flyweight: Shared font data
class Font {
    std::string name_;
    int size_;
    std::string fontData_;  // Actually several MB
public:
    Font(std::string name, int size) 
        : name_(std::move(name)), size_(size) {
        fontData_ = std::string(1000000, 'F');  // 1MB virtual data
        std::cout << "Loading font: " << name_ << " " << size_ << "pt\n";
    }
    
    void render(char c, int x, int y) const {
        std::cout << "Render '" << c << "' with " << name_ 
                  << " at (" << x << "," << y << ")\n";
    }
};
class FontFactory {
    std::unordered_map<std::string, std::shared_ptr<Font>> fonts_;
    
    std::string makeKey(const std::string& name, int size) {
        return name + "_" + std::to_string(size);
    }
public:
    std::shared_ptr<Font> getFont(const std::string& name, int size) {
        std::string key = makeKey(name, size);
        auto it = fonts_.find(key);
        if (it != fonts_.end()) return it->second;
        
        auto font = std::make_shared<Font>(name, size);
        fonts_[key] = font;
        return font;
    }
};
// Context: Character with extrinsic state
class Character {
    char ch_;
    int x_, y_;
    std::shared_ptr<Font> font_;  // Flyweight reference
public:
    Character(char ch, int x, int y, std::shared_ptr<Font> font)
        : ch_(ch), x_(x), y_(y), font_(std::move(font)) {}
    
    void draw() const {
        font_->render(ch_, x_, y_);
    }
};
int main() {
    FontFactory factory;
    
    auto arial12 = factory.getFont("Arial", 12);
    auto arial12_2 = factory.getFont("Arial", 12);  // Reuse
    auto times14 = factory.getFont("Times", 14);
    
    std::vector<Character> text;
    text.emplace_back('H', 0, 0, arial12);
    text.emplace_back('i', 10, 0, arial12_2);  // Same font
    text.emplace_back('!', 20, 0, times14);
    
    for (const auto& ch : text)
        ch.draw();
    
    // Output:
    // Loading font: Arial 12pt
    // Loading font: Times 14pt
    // Render 'H' with Arial at (0,0)
    // Render 'i' with Arial at (10,0)
    // Render '!' with Times at (20,0)
    
    return 0;
}

Key Point: Same font loaded only once, each character only has different position.

3. Game Tile System

Tilemap Flyweight

#include <unordered_map>
#include <memory>
#include <iostream>
#include <vector>
// Flyweight: Shared tile type
class TileType {
    std::string name_;
    std::string texture_;  // Actually large texture
    bool walkable_;
public:
    TileType(std::string name, std::string texture, bool walkable)
        : name_(std::move(name)), texture_(std::move(texture)), walkable_(walkable) {
        std::cout << "Loading tile type: " << name_ << '\n';
    }
    
    void render(int x, int y) const {
        std::cout << "[" << name_[0] << "]";
    }
    
    bool isWalkable() const { return walkable_; }
};
class TileFactory {
    std::unordered_map<std::string, std::shared_ptr<TileType>> types_;
public:
    std::shared_ptr<TileType> getTileType(const std::string& name) {
        auto it = types_.find(name);
        if (it != types_.end()) return it->second;
        
        // Define properties by tile type
        bool walkable = (name != "wall");
        auto type = std::make_shared<TileType>(name, name + ".png", walkable);
        types_[name] = type;
        return type;
    }
};
// Context: Tile with extrinsic state
class Tile {
    int x_, y_;
    std::shared_ptr<TileType> type_;  // Flyweight reference
public:
    Tile(int x, int y, std::shared_ptr<TileType> type)
        : x_(x), y_(y), type_(std::move(type)) {}
    
    void render() const {
        type_->render(x_, y_);
    }
    
    bool isWalkable() const {
        return type_->isWalkable();
    }
};
class TileMap {
    std::vector<std::vector<Tile>> tiles_;
    TileFactory factory_;
public:
    TileMap(int width, int height) {
        // Simple map creation
        for (int y = 0; y < height; ++y) {
            std::vector<Tile> row;
            for (int x = 0; x < width; ++x) {
                std::string type = (x == 0 || x == width-1 || y == 0 || y == height-1) 
                    ? "wall" : "grass";
                row.emplace_back(x, y, factory_.getTileType(type));
            }
            tiles_.push_back(std::move(row));
        }
    }
    
    void render() const {
        for (const auto& row : tiles_) {
            for (const auto& tile : row)
                tile.render();
            std::cout << '\n';
        }
    }
};
int main() {
    TileMap map(10, 5);
    map.render();
    
    // Output:
    // Loading tile type: wall
    // Loading tile type: grass
    // [W][W][W][W][W][W][W][W][W][W]
    // [W][G][G][G][G][G][G][G][G][W]
    // [W][G][G][G][G][G][G][G][G][W]
    // [W][G][G][G][G][G][G][G][G][W]
    // [W][W][W][W][W][W][W][W][W][W]
    
    return 0;
}

Key Point: Even with 50 tiles, only 2 tile types (wall, grass) are loaded.

4. Common Issues and Solutions

Issue 1: Modifying Flyweight

// ❌ Bad: Modifying shared object
auto glyph = factory.get('A');
glyph->width = 20;  // All 'A's affected!

Solution: Make Flyweight immutable. Below is an implementation example using C++. Define a class to encapsulate data and functionality. Try running the code directly to check its operation.

// ✅ Good: Only const methods
class Glyph {
    const int width_, height_;
public:
    int getWidth() const { return width_; }  // const only
};

Issue 2: Excessive extrinsic State

Here is a simple C++ code example. Try running the code directly to check its operation.

// ❌ Bad: Too much extrinsic
void draw(Glyph* g, int x, int y, int r, int g, int b, float rotation, float scale) {
    // Passing 8 arguments every time
}

Solution: Bundle extrinsic state into struct. Below is an implementation example using C++. Define a class to encapsulate data and functionality. Try running the code directly to check its operation.

// ✅ Good
struct RenderContext {
    int x, y;
    Color color;
    float rotation, scale;
};
void draw(Glyph* g, const RenderContext& ctx);

Issue 3: Memory Leak

Here is a simple C++ code example. Define a class to encapsulate data and functionality. Try running the code directly to check its operation.

// ❌ Bad: Factory keeps growing
class Factory {
    std::unordered_map<std::string, Flyweight*> cache_;  // Kept forever
};

Solution: Use LRU cache or weak_ptr. Here is detailed implementation code using C++. Define a class to encapsulate data and functionality, perform branching with conditionals. Understand the role of each part while examining the code.

// ✅ Good: Auto cleanup with weak_ptr
class Factory {
    std::unordered_map<std::string, std::weak_ptr<Flyweight>> cache_;
public:
    std::shared_ptr<Flyweight> get(const std::string& key) {
        auto it = cache_.find(key);
        if (it != cache_.end()) {
            if (auto sp = it->second.lock())
                return sp;
        }
        auto fw = std::make_shared<Flyweight>(key);
        cache_[key] = fw;
        return fw;
    }
};

Summary

Key Points

  1. Flyweight: Share common state (intrinsic)
  2. Context: Keep individual state (extrinsic) separate
  3. Factory: Manage and cache Flyweights
  4. Immutability: Flyweights should be immutable
  5. Memory savings: Effective when many similar objects

When to Use

Use Flyweight when:

  • Many similar objects
  • Common state is large
  • Individual state is small
  • Memory is limited ❌ Don’t use when:
  • Few objects
  • All state is unique
  • Complexity outweighs benefits

Best Practices

  • ✅ Make Flyweights immutable
  • ✅ Use smart pointers for memory management
  • ✅ Bundle extrinsic state into structs
  • ✅ Consider weak_ptr for cache cleanup
  • ❌ Don’t modify shared state
  • ❌ Don’t make everything Flyweight