Blog 2026 08 19 C++26: std::polymorphic
Post
Cancel

C++26: std::polymorphic

In the previous article, we looked at std::indirect — C++26’s answer to the “I need a heap-allocated member with value semantics” problem. indirect<T> always stores exactly a T. But what if you need to store a Circle or a Rectangle through a Shape base class — and copy it without slicing?

That’s std::polymorphic<T>, the other half of P3019R14. It owns a heap-allocated object whose dynamic type may be T or any type derived from T. Copying performs a type-erased deep copy that preserves the dynamic type. No virtual clone() needed.

The clone() tax

To have a copyable collection of polymorphic objects, every derived class needs a virtual clone() method. Let’s see what that looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Shape {
public:
    virtual ~Shape() = default;
    virtual std::unique_ptr<Shape> clone() const = 0;
    virtual double area() const = 0;
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    std::unique_ptr<Shape> clone() const override {
        return std::make_unique<Circle>(*this);
    }
    double area() const override { return 3.14159 * radius_ * radius_; }
};

class Rectangle : public Shape {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    std::unique_ptr<Shape> clone() const override {
        return std::make_unique<Rectangle>(*this);
    }
    double area() const override { return w_ * h_; }
};

Now you want a Picture that owns a collection of shapes with value semantics:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// full example at https://godbolt.org/z/Evz8b7r8o
class Picture {
    std::vector<std::unique_ptr<Shape>> shapes_;
public:
    Picture(const Picture& other) {
        shapes_.reserve(other.shapes_.size());
        for (const auto& s : other.shapes_)
            shapes_.push_back(s->clone());
    }
    Picture& operator=(const Picture& other) {
        auto tmp = other;          // copy-and-swap
        swap(shapes_, tmp.shapes_);
        return *this;
    }
    // ... move constructor, move assignment, destructor
};

Every new shape has to implement clone(). Forget it once and you get slicing. The Picture class needs all five special member functions written by hand. This is a lot of ceremony for something conceptually simple: “I want a copyable collection of polymorphic objects.”

Dropping the boilerplate with std::polymorphic

std::polymorphic<T> eliminates all of that. The shapes don’t need clone(), Picture doesn’t need any special member functions, and — perhaps surprisingly — Shape doesn’t even need a virtual destructor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <memory>

class Shape {
protected:
    ~Shape() = default;  // no virtual destructor needed!
public:
    virtual double area() const = 0;
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    double area() const override { return 3.14159 * radius_ * radius_; }
};

class Rectangle : public Shape {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double area() const override { return w_ * h_; }
};

class Picture {
    std::vector<std::polymorphic<Shape>> shapes_;
public:
    void add_circle(double r) {
        shapes_.emplace_back(std::in_place_type<Circle>, r);
    }
    void add_rectangle(double w, double h) {
        shapes_.emplace_back(std::in_place_type<Rectangle>, w, h);
    }

    double total_area() const {
        double sum = 0;
        for (const auto& s : shapes_)
            sum += s->area();
        return sum;
    }

    size_t size() const { return shapes_.size(); }

    // ALL special member functions are compiler-generated.
    // Copying deep-copies every shape, preserving its dynamic type.
};

No clone(). No Rule of Five. Copying a Picture copies every shape — a Circle is copied as a Circle, a Rectangle as a Rectangle. The type-erasure machinery inside polymorphic handles this automatically. The only requirement is that every stored type must be copy-constructible.

Deep copies just work

1
2
3
4
5
6
7
8
9
10
Picture a;
a.add_circle(5.0);
a.add_rectangle(3.0, 4.0);

Picture b = a;  // deep copies both shapes, preserving their dynamic types
a.add_circle(1.0);

assert(a.size() == 3);  // a has three shapes
assert(b.size() == 2);  // b is unchanged — it's an independent copy
assert(a.total_area() != b.total_area());

With unique_ptr this would require the hand-written copy constructor we saw above.

No virtual destructor needed

Notice that Shape’s destructor is protected and non-virtual in the polymorphic version. Because polymorphic uses type erasure for both destruction and copying, it knows the actual dynamic type and can call the right destructor directly — no vtable dispatch needed. This is a genuine safety improvement: you can’t accidentally delete a Shape* and get undefined behaviour, because nobody ever holds a raw Shape*.

Const propagation

Just like std::indirect, polymorphic propagates const correctly. Accessing through a const polymorphic<Shape>& yields a const Shape&. In the Picture example, total_area() is a const method, and s->area() correctly resolves to the const overload of area(). If Shape had a non-const mutating method, calling it through a const Picture would be a compile error — the same improvement over unique_ptr that we discussed in the previous article.

What polymorphic does not provide

Unlike indirect, polymorphic has no comparison operators and no hash support. The reason is straightforward: the dynamic type is erased. There is no way to forward == or <=> to the actual stored object without requiring virtual comparison methods on the base class, which the committee considered out of scope.

There is also no perfect-forwarded assignment (operator=(U&&)) — again because the type information is erased at runtime.

The valueless state

Like indirect, polymorphic has no null state. A polymorphic<T> always owns an object — except after being moved from, where valueless_after_move() returns true. If you need nullable polymorphic indirection, use std::optional<std::polymorphic<T>>.

Choosing between indirect and polymorphic

Now that we’ve seen both types, here’s a quick summary of when to use which:

ScenarioType
PIMPL, recursive types, large membersstd::indirect<T>
Need comparison/hash on the owned objectstd::indirect<T>
Open set of derived types, polymorphic collectionsstd::polymorphic<T>
Closed set of known typesstd::variant<A, B, C>
Nullable indirectionstd::optional<indirect<T>> or std::optional<polymorphic<T>>
Non-owning, shared accessstd::shared_ptr<T> (as before)

The simplest mental model: indirect is for when you know the exact type but need it on the heap. polymorphic is for when you don’t.

Conclusion

std::polymorphic eliminates the most tedious pattern in object-oriented C++: writing clone() methods and Rule of Five boilerplate just to have a copyable collection of polymorphic objects. The type-erasure approach means no virtual destructor, no virtual clone(), and no hand-written special member functions. Together with std::indirect, it completes the value-semantic indirection story that has been missing from the standard library since smart pointers were introduced.

Connect deeper

If you liked this article, please

This post is licensed under CC BY 4.0 by the author.

For C++ developers who give a damn

Better code. Better career. One email a week on modern C++ and what it takes to grow — written by someone still figuring it out too.

    We won't send you spam. Unsubscribe at any time.