Flyweight

Learning Objectives

Share intrinsic state among many objects; keep extrinsic state outside.

Measure memory savings and keep shared state immutable.

Prerequisites: Modules 10, 13.

Intuition and Definition

Intent: use sharing to support large numbers of fine-grained objects efficiently.

Intrinsic state — shared, context-independent, immutable (a glyph's shape, a tree species' mesh).

Extrinsic state — per-use, supplied by the client (position, colour tint).

Participants: Flyweight (immutable intrinsic data), FlyweightFactory (caches and returns shared instances), client objects holding extrinsic state plus a reference to a flyweight.

Why It Matters

A map with 100,000 trees doesn't need 100,000 copies of the same mesh; a text editor doesn't need a font bitmap per character.

Syntax and Language Notes

Language runtimes use flyweights internally: Java/C# string interning, Python's small-integer cache, std::string_view into a shared buffer.

Complete Example

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

struct TreeModel {                                   // intrinsic, immutable, shared
    const std::string species;
    const std::vector<float> mesh;                   // pretend: large geometry
    TreeModel(std::string s, size_t vertices) : species(std::move(s)), mesh(vertices, 1.0f) {}
};

class TreeFactory {                                  // flyweight factory
    std::map<std::string, std::shared_ptr<const TreeModel>> cache_;
public:
    std::shared_ptr<const TreeModel> get(const std::string& species) {
        auto& slot = cache_[species];
        if (!slot) slot = std::make_shared<const TreeModel>(species, 50'000);
        return slot;
    }
    size_t models() const { return cache_.size(); }
};

struct Tree {                                        // extrinsic state + pointer to the flyweight
    float x, y, scale;
    std::shared_ptr<const TreeModel> model;
};

int main() {
    TreeFactory factory;
    std::vector<Tree> forest;
    const char* species[] = {"oak", "pine", "birch"};
    for (int i = 0; i < 30'000; ++i)
        forest.push_back({float(i % 300), float(i / 300), 1.0f + f
…
trees: 30000, distinct models: 3
mesh memory shared: 585 KiB vs one-per-tree: 5722 MiB
same model object for trees 0 and 3: 1

Step-by-Step Walkthrough

The factory creates one TreeModel per species and hands out shared pointers to it.

Each Tree stores only what differs (position, scale) — a few bytes plus a pointer.

3 meshes × 200,000 bytes ≈ 585 KiB instead of 30,000 × 200,000 bytes ≈ 5,722 MiB (about 5.6 GiB).

The model is const: sharing mutable state would let one tree change all others.

Common Mistakes

Mutable flyweights — a change through one reference affects every user.

Putting extrinsic state into the flyweight, which breaks sharing.

Flyweights for few objects — the factory and indirection cost more than they save.

Thread safety of the factory — concurrent get needs a lock (the shared, immutable model itself is safe to read).

Trade-offs and Alternatives

Flyweight trades CPU (lookups, extrinsic state passed around) for memory. Data-oriented alternatives (arrays of positions + a species index) can be even cheaper and more cache-friendly (Module 70).

Real-World Use

Glyph caches in text rendering, game instancing (one mesh, many transforms), string interning, icon caches, and shared immutable configuration across thousands of devices in a fleet simulator.

Practice

Q1. (Easy) Intrinsic or extrinsic: a character's position on the page?

Answer: Extrinsic.

Q2. (Medium) Why must flyweights be immutable?

Answer: They're shared; mutating one would change every object that uses it.

Q3. (Advanced) How much memory do the Tree objects themselves take on this platform, and why is the pointer the biggest part?

Answer: On a typical 64-bit platform Tree is 3 floats (12 bytes) + padding to 16 + a shared_ptr (16 bytes) ≈ 32 bytes, so 30,000 trees ≈ 940 KiB. A plain index (uint8_t species) instead of a shared_ptr would cut that roughly in half — a data-oriented refinement.

Interview Question

Q1. "Flyweight vs caching?" Answer: Both reuse objects, but a cache stores results to avoid recomputation and may evict them; a flyweight factory shares immutable intrinsic state among many logical objects to save memory and must keep each flyweight alive while it is referenced. Flyweights also split state into intrinsic and extrinsic parts, which a plain cache doesn't require.

Quick Quiz

Q1. What does the flyweight factory do?

Answer: Returns an existing shared instance for a key, creating it only once.

Q2. Who supplies extrinsic state?

Answer: The client (context) object.

Q3. Name a runtime feature that uses flyweights.

Answer: String interning.

Summary

Flyweight shares immutable intrinsic state; clients keep extrinsic state.

Worth it for very many similar objects.

Related: Modules 31, 70.