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 howstd::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
- 2. Structure
- 3. C++ Implementations
- 4. Deep vs Shallow Copy
- 5. When to Use, When Not To
- 6. Pitfalls
- 7. Related Patterns
- 8. References
1. Intent
- Create a new object by copying an existing one rather than calling a constructor.
- Polymorphic copy — given
Base*, produce anotherBase*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_ptrslices.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 inheritsclone()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 whatclone()should produce; - copies
std::vector,std::string, etc. — deep, fine.
- copies values (
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_ptrwith explicit clone). - For
unique_ptrmembers → write the deep-copy explicitly inclone()(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 copy —
Base*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 copy —
Shape s = *circle_ptr;slices toShape. Always work through pointer/reference +clone(). MakeShapeabstract (pure virtualdraw()) to prevent it at compile time. - Forgotten
clone()override — derived class inherits base'sclone(), 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_ptrmembers — doesclone()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 → considerstd::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
- Prototype — refactoring.guru
- Prototype — sourcemaking
- GoF, Design Patterns, ch. 3 — Prototype (p. 117)
- cppreference — Rule of three/five/zero
- cppreference —
std::variant - Herb Sutter, "Virtual Constructors"
- Scott Meyers, More Effective C++, Item 25 — "Virtualizing constructors and non-member functions"