Skip to content
C++
Idiom
since C++11
Basic

Prototype Pattern

C++ prototype pattern — virtual clone with unique_ptr, CRTP clone mixin, deep vs shallow copy, and prototype registry.

Prototype Patternsince C++11

Create 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):

cpp
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

cpp
#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:

cpp
// 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:

cpp
#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 object

insert_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:

cpp
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.

cpp
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:

cpp
// 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.


See Also

  • CRTP — the compile-time technique powering the clone mixin
  • Move Semantics — complement to copy; know when to move instead of clone
  • RAIIunique_ptr ownership model that makes clone() safe
  • Factory Method — alternative creation strategy when cloning a baseline is not required