← Back to list

Copying Polymorphic Types: How C++26 simplifies value semantics

A standard library vocabulary type that gives heap-allocated objects deep-copy and const-propagating value semantics.

Sagar in Towards Dev · 2026-06-25 03:44 · 1 claps · 7.9 min read
#programming #software-development #cpp26 #c-plus-plus-language #technology
Open on Medium ↗
Wiki topics: LNG · Linguistics & Language 💻 · Programming 📚 · Books & Reading

Copying Polymorphic Types: How C++26 simplifies value semantics

A standard library vocabulary type that gives heap-allocated objects deep-copy and const-propagating value semantics.

C++ has always had a strange gap in its design. You can write polymorphic classes, you can store them through pointers, and you can make them behave nicely at runtime. But the moment you try to treat those objects like ordinary values — copy them, put them in containers, pass them around freely — the design starts to get awkward fast.

That awkwardness shows up everywhere: object slicing, **unique_ptr containers that refuse to copy, and hand-written `clone()`** functions that seem to appear in every non-trivial hierarchy. For years, C++ developers have had to stitch together their own solutions just to get something that feels like normal value semantics.

C++26’s **std::polymorphic** aims to clean that up.

Object Slicing Recap

Let’s say you are making a game. You have a basic Enemy, and a tougher Boss that inherits from it. You want to store them all in a std::vector so you can loop through and update them.

The natural instinct is to do this:

#include <iostream>
#include <vector>

class Enemy {
public:
    virtual ~Enemy() = default;
    virtual void attack() const { 
        std::cout << "Basic punch! (10 damage)\n"; 
    }
};

class Boss : public Enemy {
    int fire_damage = 50; // Extra data only the Boss has
public:
    void attack() const override { 
        std::cout << "Fire breath! (" << fire_damage << " damage)\n"; 
    }
};

int main() {
    std::vector<Enemy> level_enemies;

    Boss big_dragon;

    // THE TRAP: We push the Boss into a vector of regular Enemies.
    level_enemies.push_back(big_dragon); 

    // Outputs: "Basic punch! (10 damage)" ... Wait, what?
    level_enemies[0].attack(); 

    return 0;
}

What happened? Why did the Dragon suddenly behave like a regular enemy?

This is a problem known as object slicing.

When you create a **std::vector<Enemy>, the vector can only store objects that are the size of an `Enemy**. If you try to put aBoss` object into it, C++ copies only the **Enemy** part of that object and discards everything that belongs specifically to Boss.

As a result, any extra data or behavior defined in **Boss is lost, and the object effectively becomes a plain `Enemy`**. That's why your Dragon no longer acts like a boss—it has been sliced down to its base class.

The Workaround We’ve Been Using: Pointers

To fix object slicing, we are taught to never store polymorphic objects by value. Instead, we store pointers.

If a vector just holds memory addresses (pointers), then every item in the vector is the same size. The actual enemies live freely on the heap, keeping all their special subclass data intact.

In modern C++, we use **std::unique_ptr** to manage this memory safely:

#include <iostream>
#include <vector>
#include <memory>

// ... (Enemy and Boss classes are the same as above) ...

int main() {
    // We store pointers now! No more slicing.
    std::vector<std::unique_ptr<Enemy>> level_enemies;

    level_enemies.push_back(std::make_unique<Boss>());

    // Outputs: "Fire breath! (50 damage)" - It works!
    level_enemies[0]->attack(); 

    return 0;
}

Problem solved, right? We fixed the slicing. But solving slicing introduces another issue: copying.

The New Challenge: Copying Polymorphic Objects

What happens if you want to save the game? Or undo an action? You need to make a copy of that **level_enemies** vector.

// Try to make a backup
std::vector<std::unique_ptr<Enemy>> backup = level_enemies; 
       // ERROR! WILL NOT COMPILE.

**std::unique_ptr refuses to be copied, because two unique pointers can't own the same memory. `std::unique_ptr`** is for exclusive ownership and not copyable, only movable.

To fix this, we are forced to write a virtual **clone() method. We have to go into `Enemy**, addvirtual std::unique_ptr<Enemy> clone() const = 0;`. Then we have to go into **Boss** and implement it. If we have 50 different enemy types, we have to write this exact same boilerplate code 50 times.

Then, to copy the vector, we have to write a manual **for loop that calls `.clone()`** on every single item.

Many teams build their own abstractions to reduce this boilerplate — using CRTP helpers, clone mixins, type erasure wrappers, or custom polymorphic_value implementations—but every project ends up reinventing a similar solution. These solutions work, but they add complexity and a lot of work.

After decades of writing **clone() methods, C++26 introduces `std::polymorphic<T>`**.

C++26: std::polymorphic

**std::polymorphic<T>** in C++26 is designed to package these ideas into a standard, value-like abstraction:

  1. It allocates memory on the heap (just like a pointer) so slicing never happens.
  2. But it behaves exactly like a normal value (like an **int**). If you copy it, it automatically clones the underlying object for you.

In many common cases, you no longer need to implement a custom clone() hierarchy yourself. The standard library keeps track of the concrete type stored inside and automatically performs the correct deep copy when the object is copied.

Here is how clean our game code looks in C++26:

#include <iostream>
#include <vector>
#include <polymorphic> // C++26 header

class Enemy {
public:
    virtual ~Enemy() = default;
    virtual void attack() const { std::cout << "Basic punch!\n"; }
};

class Boss : public Enemy {
public:
    void attack() const override { std::cout << "Fire breath!\n"; }
};

int main() {
    std::vector<std::polymorphic<Enemy>> level_enemies;

    // We tell it to build a Boss on the heap.
    // std::in_place_type is just a tag telling the compiler what class to make.
    level_enemies.push_back(std::polymorphic<Enemy>(std::in_place_type<Boss>));

    // We access it with an arrow -> just like a pointer.
    level_enemies[0]->attack(); // Outputs: Fire breath!

    // The copy operation automatically preserves the dynamic type
    // of every stored object.
    // Every single polymorphic object inside automatically deep-copies itself.
    // No clone() methods, no manual for-loops.
    std::vector<std::polymorphic<Enemy>> backup = level_enemies;

    backup[0]->attack(); // Outputs: Fire breath!

    return 0;
}

By combining heap allocation with value semantics, **std::polymorphic** removes an immense amount of boilerplate from C++ codebases.

You no longer have to worry about object slicing, and you no longer have to implement complex clone mixins just to copy a list of objects. You simply put your base class in a **std::polymorphic** wrapper, and let the standard library handle the underlying mechanics.

It is also worth noting what std::polymorphic is not. The goal was never to create another smart pointer. Earlier revisions explored more pointer-like interfaces, but the final design deliberately embraces value semantics. A std::polymorphic object owns a dynamically allocated object, but it behaves like a value rather than a reference.

That distinction is what allows copying, assignment, and ownership to feel natural while still avoiding object slicing.

One interesting design decision is that std::polymorphic does not allow implicit conversions from derived objects. For example, the following is intentionally ill-formed:

Boss boss_enemy;

// Error: implicit conversion not allowed
std::polymorphic<Enemy> enemy = boss_enemy;

// OK: allocation is explicit
std::polymorphic<Shape> p2(std::in_place_type<Circle>);

Constructing a std::polymorphic may require heap allocation, and the committee decided that such allocations should always be explicit.

Reduce the need for virtual destruction in designs

If you look closely at standard C++ polymorphism, there is one more piece of boilerplate we are forced to write: the virtual destructor. If you delete a derived class through a base class pointer without a virtual destructor, you trigger undefined behavior.

**std::polymorphic** quietly solves this, too.

Because **std::polymorphic uses type erasure when it is constructed, it remembers exactly which derived class it holds. When it goes out of scope, it automatically calls the correct derived destructor. Your base class interface no longer needs a public virtual destructor. In fact, the paper authors recommend using a `protected`** non-virtual destructor to prevent accidental deletion via raw pointers entirely:

class Enemy {
protected:
    // No 'virtual' needed! std::polymorphic remembers the exact type.
    ~Enemy() = default; 
    Enemy(const Enemy&) = default;
public:
    virtual void attack() const = 0;
};

This guarantees that the lifecycle of the object is strictly managed by the **std::polymorphic** wrapper, resulting in safer, tighter class designs.

The Sibling Type: std::indirect and the Pimpl Idiom

The P3019 proposal actually introduces two vocabulary types.

  • While **std::polymorphic** handles class hierarchies,
  • its sibling type **std::indirect<T>** is designed for single, non-polymorphic types.

Why would you allocate a non-polymorphic type on the heap but treat it like a value? The Pimpl (Pointer-to-Implementation) Idiom.

When hiding implementation details to speed up compilation times, developers traditionally use **std::unique_ptr<Impl>. Just like with our `Enemy** hierarchy, this breaks value semantics. If you want your public class to be copyable, you have to write a custom copy constructor and assignment operator in your.cpp` file to manually copy the data inside the **unique_ptr**.

**std::indirect** eliminates this completely. It supports incomplete types, manages the heap allocation, and automatically generates deep copies.

// NetworkClient.h
#include <polymorphic> // std::indirect lives here too

class NetworkClient {
    class Impl; // Forward declaration

    // Stores the implementation on the heap, but behaves like a value!
    std::indirect<Impl> impl_; 
public:
    NetworkClient();

    // We do NOT need to write a custom copy constructor. 
    // The compiler generates it, and std::indirect deep-copies the Impl.
};

Together, std::polymorphic and std::indirect completely decouple the concept of "heap allocation" from "reference semantics" (pointers).

A crucial safety warning: The Valueless State

Because these types act like values, what happens if you explicitly use **std::move()** on them?

Moving a **std::polymorphic or `std::indirect`** object does not allocate new memory. It efficiently transfers ownership of the underlying heap allocation to the new object.

However, just like **std::unique_ptr becomes `nullptr** after a move, a moved-fromstd::polymorphic` enters a "valueless" state. The standard library provides a **.valueless_after_move()** method to check this. Dereferencing a valueless object is undefined behavior, so if you are moving these objects around, treat them with the same care you would a moved-from smart pointer.

The Standardization of this Feature (P3019)

For those interested in the standardization process, this feature was formally accepted into C++26 via proposal P3019: Vocabulary Types for Composite Class Design by authors Jonathan Coe, Antony Peacock, and others.

The journey to standardizing this feature actually started years earlier with P0201 (**polymorphic_value**), which served as the primary incubator for the idea. The committee recognized that providing value semantics for dynamically allocated objects was a missing piece of the standard library. P3019 finalized the design by introducing two distinct vocabulary types:

  • **std::polymorphic<T>**: For objects requiring dynamic dispatch (virtual functions).
  • **std::indirect<T>**: A sibling type for non-polymorphic incomplete types, designed to cleanly replace boilerplate in the Pimpl idiom.

Thanks to the work in these proposals, C++ developers finally have a standardized way to decouple dynamic allocation and polymorphism from reference semantics.

A natural question might come to your mind is: why didn’t the committee create a single type that handles both use cases?

The answer is that polymorphic objects have different requirements from ordinary objects. Supporting dynamic dispatch requires additional machinery to preserve the object’s dynamic type during copying. Rather than imposing that overhead on every use case, C++26 introduces two separate vocabulary types.

  • Use std::indirect<T>when you simply want a heap-allocated object with value semantics.
  • Use std::polymorphic<T>when you need runtime polymorphism through a base class.

The proposal also discusses implementation techniques such as Small Buffer Optimization (SBO), similar to std::function. Rather than standardizing a buffer size, the committee left such optimizations up to library implementers.

Note: Like any heap-based abstraction, std::polymorphic introduces allocation overhead, so it is not a universal replacement for every polymorphic design.

Closing Thoughts

While reading through P3019, what stood out to me was that it doesn’t really introduce a brand-new idea. Instead, it takes a pattern that many C++ developers have been building themselves for years and finally makes it part of the standard library. That’s often where some of the most useful language improvements come from.

s**td::polymorphic**is one of those features. It won’t completely change the way you write C++, but it can remove a surprising amount of boilerplate and make working with polymorphic objects feel much more natural.

Found this article helpful? Please Clap 👏 and follow for more C++ and system programming content.


메타데이터
post_id
de1f0abdcbfa
slug
std-polymorphic-cpp26-value-semantics-de1f0abdcbfa
url
https://towardsdev.com/std-polymorphic-cpp26-value-semantics-de1f0abdcbfa
canonical_url
https://towardsdev.com/std-polymorphic-cpp26-value-semantics-de1f0abdcbfa
author_url
https://medium.com/@sagarmadala
status
ok
fetched_at
2026-06-26 06:47:43