Prototype Pattern


  • Description: Create new objects by cloning an existing instance — virtual clone() for polymorphic copy; covers deep vs shallow copy, prototype registries, copy-and-swap, and how std::variant + value semantics can replace it.
  • My Notion Note ID: K2C-2-5
  • 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. Intent

  • Create a new object by copying an existing one rather than calling a constructor.
  • Polymorphic copy — given Base*, produce another Base* whose dynamic type matches the source, without naming the derived class.
  • Useful when:
    • construction is expensive (heavy initialization, network/file load) but copy is cheap;
    • the configuration of an existing instance is the spec for the new one;
    • the concrete type isn't known at the call site (only the base interface is visible).

2. Structure

Role Responsibility
Prototype Interface declaring clone() (and often clone_into(...)).
ConcretePrototype Implements clone() returning a same-type copy.
Client Holds a Prototype* (or registry of them); calls clone() to manufacture new objects.
Registry (optional) Maps key → prototypical instance; lookup + clone() per request.
  • C++'s built-in copy ctor is not polymorphic — Base b = *derived_ptr slices. clone() solves this.

3. C++ Implementations

3.1 Classic virtual clone()

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

struct Shape {
    virtual ~Shape() = default;
    virtual std::unique_ptr<Shape> clone() const = 0;
    virtual void draw() const = 0;
};

struct Circle : Shape {
    double r;
    explicit Circle(double r) : r{r} {}

    std::unique_ptr<Shape> clone() const override {
        return std::make_unique<Circle>(*this);
    }
    void draw() const override { std::cout << "circle r=" << r << "\n"; }
};

struct Square : Shape {
    double side;
    explicit Square(double s) : side{s} {}

    std::unique_ptr<Shape> clone() const override {
        return std::make_unique<Square>(*this);
    }
    void draw() const override { std::cout << "square s=" << side << "\n"; }
};

// Client — knows only Shape.
void duplicateAll(const std::vector<std::unique_ptr<Shape>>& src,
                  std::vector<std::unique_ptr<Shape>>& dst) {
    for (const auto& s : src) dst.push_back(s->clone());
}
  • Every derived class implements clone() identically — boilerplate. CRTP eliminates it (§ 3.3).

3.2 Covariant return type

struct Circle2 : Shape {
    Circle2* clone_raw() const { return new Circle2(*this); }
};
  • Stick with unique_ptr<Shape> returns; covariance with smart pointers needs CRTP gymnastics that aren't worth it.

3.3 CRTP to remove boilerplate

template <class Derived, class Base>
struct Cloneable : Base {
    using Base::Base;
    std::unique_ptr<Base> clone() const override {
        return std::make_unique<Derived>(static_cast<const Derived&>(*this));
    }
};

struct Triangle : Cloneable<Triangle, Shape> {
    double a, b, c;
    Triangle(double a, double b, double c) : a{a}, b{b}, c{c} {}
    void draw() const override { std::cout << "triangle\n"; }
};
  • One Cloneable<> definition; every derived class inherits clone() for free.

3.4 Prototype registry

#include <unordered_map>

class ShapeRegistry {
public:
    void registerProto(std::string key, std::unique_ptr<Shape> proto) {
        protos_[std::move(key)] = std::move(proto);
    }
    std::unique_ptr<Shape> create(const std::string& key) const {
        auto it = protos_.find(key);
        return it != protos_.end() ? it->second->clone() : nullptr;
    }
private:
    std::unordered_map<std::string, std::unique_ptr<Shape>> protos_;
};

ShapeRegistry reg;
reg.registerProto("small-circle", std::make_unique<Circle>(1.0));
reg.registerProto("big-square",   std::make_unique<Square>(10.0));

auto s = reg.create("small-circle");
  • Common in game dev (spawn enemies from prototype templates) and graphics editors (shape palettes).

3.5 Modern alternative — std::variant + value semantics

#include <variant>

struct CircleV { double r;    };
struct SquareV { double side; };
using AnyShape = std::variant<CircleV, SquareV>;

AnyShape s1 = CircleV{1.0};
AnyShape s2 = s1;             // built-in copy — already polymorphic across the variant
  • No clone(), no virtual dispatch, exhaustive at compile time. Trade-off: closed type set, can't be extended by downstream code.

4. Deep vs Shallow Copy

  • clone() must usually mean deep copy — the new object should be independent of the source.
  • Default compiler-generated copy ctor:
    • copies values (int, double) — fine;
    • copies raw pointers — shallow → two objects own the same memory → double-free;
    • copies unique_ptr — won't compile (deleted copy);
    • copies shared_ptr — bumps refcount → shared state, often not what clone() should produce;
    • copies std::vector, std::string, etc. — deep, fine.
struct Bad {
    int* buf;
    Bad(int n) : buf{new int[n]} {}
    // No user-defined copy ctor → compiler copies pointer → double-free in dtor.
};

struct Good {
    std::vector<int> buf;        // value-type member → default copy is deep
    explicit Good(int n) : buf(n) {}
};
  • Rule of zero — prefer member types that already do the right thing (vector, string, unique_ptr with explicit clone).
  • For unique_ptr members → write the deep-copy explicitly in clone() (or in copy ctor):
struct Node {
    int value;
    std::unique_ptr<Node> next;

    Node(const Node& other)
        : value{other.value}
        , next{other.next ? std::make_unique<Node>(*other.next) : nullptr} {}
};

5. When to Use, When Not To

Use when:

  • Construction is expensive relative to copy (loaded from disk/network, parsed, JIT-compiled).
  • The set of concrete types is open at the client (plugins, scripting, level editors).
  • You need polymorphic copyBase* in, Base* out, dynamic type preserved.
  • A registry of templates is natural (game entities, document templates).

Avoid when:

  • Concrete types are known at compile time → use plain copy ctors (std::variant § 3.5).
  • The "configuration" being cloned is small/cheap → just call the constructor.
  • Deep copy semantics are hard to specify (shared resources, observers, back-pointers) → Prototype hides the complexity but doesn't remove it.

6. Pitfalls

  • Slicing on accidental value copyShape s = *circle_ptr; slices to Shape. Always work through pointer/reference + clone(). Make Shape abstract (pure virtual draw()) to prevent it at compile time.
  • Forgotten clone() override — derived class inherits base's clone(), which constructs the base, not derived → silent wrong-type clones. CRTP (§ 3.3) prevents this.
  • Shallow copy of raw resources — see § 4. Rule of zero or write the rule-of-five.
  • Cycles in object graphs — naive clone() of a graph with cycles infinite-recurses. Need a "visited" map keyed by source address → new copy address.
  • shared_ptr members — does clone() mean share the same dependency or clone it? Document the semantics; default is share.
  • Cost of polymorphic clone — every clone() is a virtual call + heap allocation. Bulk cloning in tight loops → consider std::variant (no heap).

7. Related Patterns

  • Prototype vs Factory Method — Factory Method constructs a new instance from scratch. Prototype copies an existing one. Prototype wins when configuration is the expensive part.
  • Prototype vs Abstract Factory — Abstract Factory can be implemented using Prototype: factory holds prototypical instances per slot and clone()s on demand. Useful for data-driven family selection.
  • Prototype vs Builder — Builder constructs step by step from nothing. Prototype copies + tweaks. Builder for novel configurations; Prototype for repeated near-identical instances.
  • Prototype vs Singleton — orthogonal — but Prototype registry entries are often singletons themselves (one canonical template per key).
  • Prototype vs Memento — Memento snapshots state for undo/restore. Prototype produces independent fresh copies for use. Implementation-wise both rely on copy semantics.
  • Prototype vs Flyweight — Flyweight shares intrinsic state across many objects. Prototype copies state to isolate them. Opposite goals.

8. References