Prototype Pattern
C++ prototype pattern — virtual clone with unique_ptr, CRTP clone mixin, deep vs shallow copy, and prototype registry.
Prototype Patternsince C++11Create new objects by cloning an existing instance (the prototype), delegating object construction to the instance itself via a virtual clone() method that returns a type-correct deep copy.
Overview
The prototype pattern solves a specific problem: you hold a pointer-to-base and need a new, independent copy of whatever derived type it actually points to. A plain copy constructor requires knowing the concrete type at compile time — prototype defers that decision to runtime through virtual dispatch.
Prototype is the right tool when:
- Object construction is expensive (complex initialisation, asset loading) and you want to stamp out variants from a pre-warmed baseline.
- You need to duplicate a polymorphic object graph without a separate factory for each type.
- You're building a registry where consumers create instances by cloning named templates.
In pure C++ terms, the prototype pattern maps cleanly onto the language: the copy constructor is the prototype mechanism for value types. The virtual clone() method extends that to polymorphic hierarchies.
Syntax
The canonical signature in modern C++ returns std::unique_ptr<Base> (C++11):
struct Base {
virtual std::unique_ptr<Base> clone() const = 0;
virtual ~Base() = default;
};
struct Derived : Base {
std::unique_ptr<Base> clone() const override {
return std::make_unique<Derived>(*this); // delegates to copy constructor
}
};Why unique_ptr, not Base*? Raw-pointer clone() compiles back to C++98, but ownership is ambiguous: every call site must remember to delete the result. unique_ptr makes ownership explicit and destruction automatic (C++11).
Covariant returns don't work with smart pointers. C++ covariant return types allow a derived override to return Derived* when the base declares Base*, but this rule does not extend to unique_ptr<Derived> vs unique_ptr<Base>. The signature must stay unique_ptr<Base> throughout the hierarchy.
Examples
Virtual Clone
#include <memory>
#include <vector>
#include <print> // C++23
struct Shape {
virtual std::unique_ptr<Shape> clone() const = 0;
virtual void draw() const = 0;
virtual ~Shape() = default;
};
struct Circle : Shape {
float radius, cx, cy;
Circle(float r, float x = 0.f, float y = 0.f)
: radius{r}, cx{x}, cy{y} {}
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Circle>(*this);
}
void draw() const override {
std::println("Circle(r={:.1f} @ {:.1f},{:.1f})", radius, cx, cy); // C++23
}
};
struct Polygon : Shape {
std::vector<std::pair<float,float>> vertices;
explicit Polygon(std::vector<std::pair<float,float>> verts)
: vertices{std::move(verts)} {}
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Polygon>(*this); // vector deep-copies
}
void draw() const override {
std::println("Polygon({} verts)", vertices.size()); // C++23
}
};
// Clone a heterogeneous scene without knowing concrete types
std::vector<std::unique_ptr<Shape>> clone_scene(
const std::vector<std::unique_ptr<Shape>>& scene)
{
std::vector<std::unique_ptr<Shape>> copy;
copy.reserve(scene.size());
for (const auto& s : scene)
copy.push_back(s->clone());
return copy;
}CRTP Clone Mixin
Every concrete class repeating the same make_unique<Derived>(*this) body is noise. A CRTP (C++11) mixin eliminates it:
// Inject clone() into any Derived that inherits from Base
template<typename Derived, typename Base>
struct Cloneable : Base {
std::unique_ptr<Base> clone() const override {
return std::make_unique<Derived>(
static_cast<const Derived&>(*this)
);
}
};
struct Circle : Cloneable<Circle, Shape> {
float radius;
explicit Circle(float r) : radius{r} {}
void draw() const override { std::println("Circle({})", radius); }
// clone() is inherited — no boilerplate
};
struct Rectangle : Cloneable<Rectangle, Shape> {
float w, h;
Rectangle(float w, float h) : w{w}, h{h} {}
void draw() const override { std::println("Rect({}x{})", w, h); }
};The mixin is entirely zero-overhead: it introduces no data members and the static_cast is resolved at compile time.
Prototype Registry
A map from string keys to prototype instances lets consumers create typed objects by name — useful for plugin systems, game entities, or document templates:
#include <unordered_map>
#include <stdexcept>
class ShapeRegistry {
std::unordered_map<std::string, std::unique_ptr<Shape>> prototypes_;
public:
void register_shape(std::string name, std::unique_ptr<Shape> proto) {
prototypes_.insert_or_assign(std::move(name), std::move(proto));
}
[[nodiscard]] std::unique_ptr<Shape> create(const std::string& name) const {
auto it = prototypes_.find(name);
if (it == prototypes_.end())
throw std::out_of_range{"unknown prototype: " + name};
return it->second->clone();
}
};
// Populate once at startup
ShapeRegistry reg;
reg.register_shape("unit_circle", std::make_unique<Circle>(1.f));
reg.register_shape("hex", std::make_unique<Polygon>(hex_verts()));
// Stamp out independent instances at runtime
auto a = reg.create("unit_circle");
auto b = reg.create("unit_circle"); // independent clone, not the same objectinsert_or_assign (C++17) replaces the prototype if re-registered, which operator[] does not do cleanly with unique_ptr.
Copy Constructor as Prototype for Value Types
For non-polymorphic types, the copy constructor is the prototype — use it directly:
class Brush {
public:
std::string name;
float size;
Color color;
std::vector<uint8_t> texture; // deep-copied by default
Brush(const Brush&) = default; // C++11: explicit default
Brush& operator=(const Brush&) = default;
// Fluent variant builders — copy-on-modify
[[nodiscard]] Brush with_size(float s) const {
Brush b = *this;
b.size = s;
return b; // NRVO applies
}
[[nodiscard]] Brush with_color(Color c) const {
Brush b = *this;
b.color = c;
return b;
}
};
const Brush base{"round", 5.f, Color::Black, load_texture("round")};
auto red = base.with_color(Color::Red);
auto large = base.with_size(20.f);[[nodiscard]] (C++17) prevents accidentally discarding the modified copy.
Best Practices
Return unique_ptr<Base>, not raw pointer. Ownership is unambiguous at every call site and exceptions cannot leak. There is no runtime cost compared to a raw pointer.
Use the CRTP mixin for hierarchies with more than two concrete types. The boilerplate in manual clone() overrides is easy to get wrong silently (see Pitfalls).
Keep clone() and the copy constructor in sync. clone() calls the copy constructor; if the copy constructor is incomplete or shallow, every clone() call inherits the defect. Adding a new data member to a class must update both.
Prototype registry keys should be compile-time constants where possible. A std::string_view-keyed map, or even an enum-keyed array, prevents typo-induced runtime errors that std::out_of_range only catches at runtime.
= default is not always deep. = default on a copy constructor generates memberwise copy. For members that are raw pointers, this produces a shallow copy — both the original and the clone share the pointed-to data. Prefer std::vector, std::string, or std::unique_ptr (which requires a custom deep-copy constructor) over raw pointers.
Common Pitfalls
Forgetting override on a derived clone(). Without override, a clone() with a mismatched signature silently introduces a new non-virtual overload. The base clone() is then called, returning a base-typed slice, not the derived object. Always mark overrides with override (C++11) so the compiler catches signature mismatches.
struct Derived : Base {
std::unique_ptr<Base> Clone() const { // typo — capital C
return std::make_unique<Derived>(*this);
}
// Base::clone() is still pure virtual → linker error, or if Base
// provides a default, silently clones the wrong type
};Object slicing when assigning by value. If clone() ever returns by value or the result is assigned to a Base object (not a pointer), derived members are stripped. Always clone into a pointer:
// WRONG: slices Derived to Base
Base b_slice = *shape->clone();
// Correct: preserve the heap-allocated derived object
auto cloned = shape->clone(); // std::unique_ptr<Base>Cloning a derived class that forgot to implement clone(). If an intermediate class in a deep hierarchy doesn't override clone(), the nearest ancestor's clone() fires — returning the wrong type without a compile error. The CRTP mixin prevents this by injecting the correct clone() into every participating class automatically.
Shallow copy of pointer members. A std::vector<int*> copies the pointers, not the pointed-to integers. If clone() relies on = default, verify every member type deep-copies correctly. std::vector<int>, std::string, and std::shared_ptr deep-copy; raw pointers and std::weak_ptr do not.
CRTP-Based Prototype for Static Dispatch
CRTP lets a base class inject a type-correct clone() without virtual dispatch. Instead of declaring clone() pure virtual in the base, a CRTP mixin provides a non-virtual implementation that constructs the exact derived type at compile time.
// C++11
#include <memory>
struct Shape {
virtual void draw() const = 0;
virtual ~Shape() = default;
};
template<typename Derived>
class Cloneable {
public:
std::unique_ptr<Derived> clone() const {
return std::make_unique<Derived>(static_cast<const Derived&>(*this));
}
};
// Diamond: Circle inherits from both Cloneable<Circle> and Shape
class Circle : public Cloneable<Circle>, public Shape {
public:
float radius;
explicit Circle(float r) : radius{r} {}
void draw() const override { /* ... */ }
};The diamond inheritance issue is benign here but must be acknowledged. Circle inherits from both Cloneable<Circle> and Shape, but since Cloneable<Circle> holds no data and Shape has a virtual destructor, no ambiguity arises. If Shape itself were a CRTP base, you would need virtual inheritance to avoid duplicate subobjects. The call circle.clone() resolves statically — the Derived in Cloneable<Derived> is fixed at instantiation, so no vtable lookup occurs.
Clone with Covariant Return Types
Covariant return types allow a derived override to return a more specific raw pointer type, but this rule does not extend to smart pointers. A base that declares virtual Shape* clone() const may be overridden with Circle* clone() const override in the derived class — the compiler accepts this because Circle* is a subtype of Shape*.
// C++11 — raw pointer covariant return
struct Shape {
virtual Shape* clone() const = 0;
virtual ~Shape() = default;
};
struct Circle : Shape {
float radius;
explicit Circle(float r) : radius{r} {}
Circle* clone() const override { // covariant: Circle* ↔ Shape*
return new Circle(*this);
}
};unique_ptr<Circle> is not a subtype of unique_ptr<Shape>, so the same trick cannot be applied to smart-pointer returns — the compiler rejects it.
// DOES NOT COMPILE — unique_ptr covariance is not supported
struct Circle : Shape {
std::unique_ptr<Circle> clone() const override; // error: incompatible return type
};The standard workaround is the non-virtual / virtual split: expose a non-virtual clone() returning unique_ptr<Derived> to callers that know the concrete type, and keep a private virtual clone_impl() returning unique_ptr<Base> for polymorphic use.
// C++11 — NVI workaround for smart-pointer covariance
struct Shape {
std::unique_ptr<Shape> clone() const { return clone_impl(); }
virtual ~Shape() = default;
private:
virtual std::unique_ptr<Shape> clone_impl() const = 0;
};
struct Circle : Shape {
float radius;
explicit Circle(float r) : radius{r} {}
// Public non-virtual accessor — returns the specific type
std::unique_ptr<Circle> clone() const {
return std::make_unique<Circle>(*this);
}
private:
std::unique_ptr<Shape> clone_impl() const override {
return std::make_unique<Circle>(*this);
}
};Deep vs Shallow Copy in Prototype
A shallow copy duplicates the pointers, not the data they point to; a deep copy duplicates everything reachable from the object. The prototype pattern is only correct when clone() performs a deep copy — otherwise two "independent" clones secretly share mutable state.
// C++11 — deep copy: vector member is duplicated
#include <vector>
#include <memory>
struct ShapeWithData : Shape {
std::vector<float> data_; // value type — copy constructor deep-copies
std::unique_ptr<Shape> clone() const override {
return std::make_unique<ShapeWithData>(*this); // data_ is fully copied
}
};When the owned data is immutable, sharing it across clones is safe and eliminates the copy cost.
// C++11 — shared immutable data: pointer is copied, not the payload
struct Data { std::vector<float> values; };
struct ShapeWithSharedData : Shape {
std::shared_ptr<const Data> data_; // const → safe to share
std::unique_ptr<Shape> clone() const override {
return std::make_unique<ShapeWithSharedData>(*this);
// shared_ptr copy increments the ref-count — no heap allocation
}
};Copy-on-write sits between the two extremes. Store data behind a shared_ptr; before any mutation, check use_count() > 1 and replace the pointer with a private deep copy. This gives clone-time O(1) performance while guaranteeing isolation at the first write.
Prototype Registry Pattern
A prototype registry maps string keys to pre-constructed prototype instances; create(key) returns a clone of the registered object. This decouples object construction from object configuration: the registry owns canonical baseline instances, and callers get independent copies without needing to know any concrete type.
// C++11
#include <unordered_map>
#include <memory>
#include <stdexcept>
#include <string>
class PrototypeRegistry {
std::unordered_map<std::string, std::unique_ptr<Shape>> prototypes_;
public:
void register_prototype(std::string key, std::unique_ptr<Shape> proto) {
prototypes_[std::move(key)] = std::move(proto);
}
[[nodiscard]] std::unique_ptr<Shape> create(const std::string& key) const {
auto it = prototypes_.find(key);
if (it == prototypes_.end())
throw std::out_of_range{"PrototypeRegistry: unknown key: " + key};
return it->second->clone();
}
};
// At startup — register baseline instances
PrototypeRegistry registry;
registry.register_prototype("circle", std::make_unique<Circle>(1.f));
registry.register_prototype("rectangle", std::make_unique<Rectangle>(2.f, 1.f));
// At runtime — clone on demand, no type knowledge required
auto btn = registry.create("circle"); // independent Circle
auto panel = registry.create("rectangle"); // independent RectangleTypical use cases include GUI widget factories and game object spawn tables. A game level file names entity types by string; the registry clones the appropriate pre-configured prototype for each spawn point. Updating behavior for all future spawns of a type is then a single call to register_prototype with a new baseline.
Prototype vs Factory Method vs Builder
Prototype is the right choice when construction is expensive and instances differ only slightly from a template object. The alternatives differ in where type knowledge lives and how much flexibility is available at the call site.
| Approach | When to use | Key operation | Type known at call site? | Config flexibility |
|---|---|---|---|---|
| Prototype | Copy an existing configured instance | clone() on a base pointer | No — resolved at runtime | High — clone then mutate |
| Factory Method | Create from scratch via a virtual factory | create() dispatches to a subclass | No — virtual dispatch | Medium — subclass decides |
| Builder | Construct incrementally with many options | build() after fluent setters | Yes — concrete builder type | Very high — field by field |
| Abstract Factory | Create families of related objects | Suite of create_X() methods | No — factory is abstract | Medium — factory-level |
Prototype trades construction cost for copy cost. When the baseline object is expensive to initialize (loading assets, establishing connections, running calibration), cloning avoids repeating that work. Factory method and abstract factory always pay the full construction cost. Builder is appropriate when no single "template instance" exists — the desired configuration is assembled from scratch at every call site.
Serialization-Based Prototype (Clone via Serialize/Deserialize)
Any type that supports serialization can be cloned without a virtual clone() method: serialize to a buffer and deserialize into a fresh instance. This approach is slower than a direct copy constructor call, but it works uniformly across type hierarchies and can cross process or network boundaries.
// C++11 — clone via stringstream round-trip
#include <sstream>
#include <memory>
struct Serializable {
virtual void serialize(std::ostream& os) const = 0;
virtual void deserialize(std::istream& is) = 0;
virtual ~Serializable() = default;
};
struct Point : Serializable {
float x, y;
Point(float x = 0.f, float y = 0.f) : x{x}, y{y} {}
void serialize(std::ostream& os) const override { os << x << ' ' << y; }
void deserialize(std::istream& is) override { is >> x >> y; }
};
// Clone any Serializable without a virtual clone()
template<typename T>
T clone_via_serialization(const T& src) {
std::ostringstream oss;
src.serialize(oss);
std::istringstream iss{oss.str()};
T copy;
copy.deserialize(iss);
return copy;
}
Point original{3.f, 4.f};
Point cloned = clone_via_serialization(original); // independent copyThe primary use cases are undo/redo systems and checkpointing, where the serialized form serves double duty: it is both the clone and the persistent record of state. Unlike virtual clone(), the serialization approach does not require modifying the class hierarchy when a new type is added — as long as the type implements the serialization contract, cloning is automatic. The cost is throughput: a std::stringstream round-trip allocates memory and runs format conversion, making it unsuitable for hot paths where thousands of clones are created per frame.
See Also
- CRTP — the compile-time technique powering the clone mixin
- Move Semantics — complement to copy; know when to move instead of clone
- RAII —
unique_ptrownership model that makesclone()safe - Factory Method — alternative creation strategy when cloning a baseline is not required