Skip to content

Latest commit

 

History

History
287 lines (218 loc) · 15.5 KB

stdpolymorphic.md

File metadata and controls

287 lines (218 loc) · 15.5 KB

std::polymorphic


Executive Abstract

std::polymorphic is a smart-wrapper class template in <memory> introduced in C++26 (standardized via proposal P3019, evolving from P0201 polymorphic_value) designed to provide deep-copy value semantics for dynamically allocated class hierarchies. Prior to C++26, runtime polymorphism in C++ forced software architectures into reference/pointer semantics (std::unique_ptr<Base> or std::shared_ptr<Base>), making value-like operations—such as copying containers of polymorphic objects—impossible without extensive hand-written clone() boilerplate across every derived class.

std::polymorphic<T> owns a heap-allocated instance of a type derived from T, automatically preserves the dynamic concrete type on copy operations via type-erased cloning machinery, enforces strict const-propagation (a const std::polymorphic<T> yields a const T&), prevents object slicing, and provides explicit lifecycle mechanics including move ownership transfer and a valueless_after_move() query.


Motivation & Historical Context

Pre-C++26 Deficiencies: The Polymorphic Copying & Pointer Semantics Dilemma

Prior to C++26, C++ developers seeking to store derived objects in composite structures or standard containers faced a fundamental dilemma.

// Pre-C++26 Dilemma: Storing Polymorphic Objects in Containers
struct Shape { virtual ~Shape() = default; virtual void draw() const = 0; };
struct Circle : Shape { void draw() const override {} };
struct Square : Shape { void draw() const override {} };

// FLAW 1: Storing values directly causes Object Slicing!
// std::vector<Shape> shapes; // Fails to compile (abstract class) or slices derived state!

// FLAW 2: std::unique_ptr disables Container Copying!
std::vector<std::unique_ptr<Shape>> shapes1;
shapes1.push_back(std::make_unique<Circle>());
// std::vector<std::unique_ptr<Shape>> shapes2 = shapes1; // ERROR: std::unique_ptr is non-copyable!

// FLAW 3: std::shared_ptr breaks Value Semantics!
std::vector<std::shared_ptr<Shape>> shared1;
shared1.push_back(std::make_shared<Circle>());
auto shared2 = shared1; // Copying shares references! Mutating shared2[0] mutates shared1[0]!

Deficiencies of Pre-C++26 Alternatives:

  1. Object Slicing: Direct value containment (Shape s = Circle{}) truncates the derived portion of the object, stripping away member variables and dynamic virtual dispatch.
  2. Non-Copyable Unique Containers: Storing std::unique_ptr<Base> prevents container copy-construction. Duplicating a std::vector<std::unique_ptr<Shape>> required writing custom iteration loops and demanding that every derived class implement a custom virtual std::unique_ptr<Base> clone() const member function.
  3. Reference Aliasing Leaks: Using std::shared_ptr<Base> enables container copying, but introduces shared ownership. Modifying a shape in the copied vector inadvertently modifies the shape in the original vector, violating value isolation.
  4. Const Correctness Leaks: Accessing pointees through const std::unique_ptr<T> allows non-const method invocation on T (ptr->non_const_method()), breaking logical constness propagation.

Solution: ISO C++26 introduced std::polymorphic<T> (P3019) to grant heap-allocated class hierarchies full, copyable value semantics while preserving runtime polymorphism.


Technical Mechanics & Architecture

1. Value Semantics for Dynamic Hierarchies

             1. Pointer Semantics (std::unique_ptr<Base>)              2. Value Semantics (std::polymorphic<Base>)
   ┌──────────────────────────────────────────────────────┐       ┌──────────────────────────────────────────────────────┐
   │ unique_ptr<Base>                                     │       │ polymorphic<Base>                                    │
   │  ┌───────────┐         ┌──────────────────────────┐  │       │  ┌───────────┐         ┌──────────────────────────┐  │
   │  │ Control   ├────────►│ Heap Object (Derived)    │  │       │  │ Control   ├────────►│ Heap Object (Derived)    │  │
   │  └───────────┘         └──────────────────────────┘  │       │  └───────────┘         └──────────────────────────┘  │
   │  • Copying disabled (move-only)                      │       │  • Deep copy on value assignment/copy construction   │
   │  • Non-const pointer allows const-modifications    │       │  • Strict const propagation (const T& on deref)     │
   └──────────────────────────────────────────────────────┘       └──────────────────────────────────────────────────────┘
  • Type-Erased Deep Cloning: When a std::polymorphic<Base> object is created with a derived instance (Circle), its constructor captures the operational function pointers required to copy-construct and destroy that concrete dynamic type. On copy construction or assignment, std::polymorphic allocates new heap memory and clones the concrete derived instance without requiring virtual clone() methods.
  • Deep Value Independence: Copying a std::polymorphic<Base> yields an entirely separate heap allocation. Modifying the copy never affects the original instance.

2. Behavioral Comparison Matrix

Property std::unique_ptr<T> std::shared_ptr<T> std::polymorphic<T>
Ownership Model Exclusive Ownership Shared Ownership Exclusive Value Ownership
Copy Semantics Disabled (delete) Shallow Copy (Reference Shared) Deep Copy (Clones Concrete Type)
const Propagation No (const ptr allows mutable *ptr) No (const ptr allows mutable *ptr) Yes (const ptr yields const T&)
Nullability State Default nullptr Default nullptr Valueless state only after move
Sibling Type N/A N/A std::indirect<T> (for Pimpl/non-poly types)

3. Lifetime and valueless_after_move() State

  • std::polymorphic<T> is designed as a value type, not a nullable pointer. It does not have a default nullptr state.
  • Moving a std::polymorphic<T> transfers ownership of the allocated heap pointer to the target instance in $\mathcal{O}(1)$ time without allocation.
  • The moved-from object enters a valueless state. Querying .valueless_after_move() returns true. Dereferencing a valueless std::polymorphic instance results in undefined behavior.

Evolution Across Standards (C++11 to C++26/C++29)

┌────────────────────────────────────────────────────────────────────────┐
│ C++11 / C++14 / C++17 / C++20 / C++23                                  │
├────────────────────────────────────────────────────────────────────────┤
│ • Polymorphism required raw pointers, unique_ptr, or shared_ptr.       │
│ • Container duplication required writing manual virtual clone() methods. │
│ • Const-correctness was lost across pointer indirection boundaries.    │
└────────────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼
┌────────────────────────────────────────────────────────────────────────┐
│ C++26                                                                  │
├────────────────────────────────────────────────────────────────────────┤
│ • Introduced std::polymorphic<T> in <memory> (P3019).                   │
│ • Standardized deep-copy value semantics for class hierarchies.        │
│ • Introduced std::indirect<T> as a sibling type for non-polymorphic   │
│   Pimpl idioms and recursive value trees.                              │
│ • Enforced const-propagation and defined valueless_after_move().        │
└────────────────────────────────────────────────────────────────────────┘

Comprehensive Code Examples

Example 1: Heterogeneous Polymorphic Containers without clone() Boilerplate

Demonstrates storing polymorphic objects (Circle, Rectangle) inside a standard vector, duplicating the vector via value-copying, and illustrating const-propagation.

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

// C++26 feature check for std::polymorphic
#if defined(__cpp_lib_polymorphic) && __cpp_lib_polymorphic >= 202406L

class GraphicElement {
public:
    virtual ~GraphicElement() = default;
    virtual void render() const = 0;
    virtual void scale(double factor) = 0;
};

class Circle : public GraphicElement {
public:
    explicit Circle(double radius) : radius_(radius) {}

    void render() const override {
        std::cout << "Circle (radius: " << radius_ << ")\n";
    }

    void scale(double factor) override {
        radius_ *= factor;
    }

private:
    double radius_;
};

class Rectangle : public GraphicElement {
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}

    void render() const override {
        std::cout << "Rectangle (" << width_ << " x " << height_ << ")\n";
    }

    void scale(double factor) override {
        width_ *= factor;
        height_ *= factor;
    }

private:
    double width_;
    double height_;
};

void draw_scene(const std::vector<std::polymorphic<GraphicElement>>& scene) {
    std::cout << "--- Rendering Scene ---\n";
    for (const auto& elem : scene) {
        // Enforces const propagation: elem dereferences to const GraphicElement&
        elem->render();
    }
}

int main() {
    // 1. Construct vector holding polymorphic value objects directly
    std::vector<std::polymorphic<GraphicElement>> primary_scene;
    primary_scene.emplace_back(std::in_place_type<Circle>, 5.0);
    primary_scene.emplace_back(std::in_place_type<Rectangle>, 4.0, 6.0);

    // 2. Perform deep value copy of the entire container!
    // No hand-rolled clone() loops needed; each derived object is deep-cloned.
    auto cloned_scene = primary_scene;

    // 3. Mutate the cloned scene elements
    cloned_scene[0]->scale(2.0); // Circle radius becomes 10.0

    // 4. Verify value isolation between original and cloned scenes
    std::cout << "Original Scene (Unmodified):\n";
    draw_scene(primary_scene);

    std::cout << "\nCloned Scene (Scaled 2x):\n";
    draw_scene(cloned_scene);

    return 0;
}
#else
int main() {
    std::cout << "std::polymorphic is available in C++26 standard library implementations.\n";
    return 0;
}
#endif

Example 2: Move Semantics, valueless_after_move(), and In-Place Initialization

Demonstrates explicit lifetime rules, transfers of ownership via move semantics, and defensive checking using valueless_after_move().

#include <iostream>
#include <memory>
#include <cassert>

#if defined(__cpp_lib_polymorphic) && __cpp_lib_polymorphic >= 202406L

struct Node {
    virtual ~Node() = default;
    virtual int evaluate() const = 0;
};

struct LiteralNode : public Node {
    int value;
    explicit LiteralNode(int v) : value(v) {}
    int evaluate() const override { return value; }
};

int main() {
    // 1. Construct using in-place constructor
    std::polymorphic<Node> node_a(std::in_place_type<LiteralNode>, 42);

    std::cout << "node_a evaluation: " << node_a->evaluate() << "\n";
    std::cout << "node_a valueless: " << std::boolalpha << node_a.valueless_after_move() << "\n";

    // 2. Perform Move Construction
    std::polymorphic<Node> node_b = std::move(node_a);

    // 3. Verify transferred ownership and valueless source state
    std::cout << "\n--- After Move ---\n";
    std::cout << "node_b evaluation: " << node_b->evaluate() << "\n";
    std::cout << "node_a valueless_after_move(): " << node_a.valueless_after_move() << "\n";
    std::cout << "node_b valueless_after_move(): " << node_b.valueless_after_move() << "\n";

    if (node_a.valueless_after_move()) {
        std::cout << "SUCCESS: Moved-from std::polymorphic correctly marked as valueless.\n";
    }

    return 0;
}
#else
int main() { return 0; }
#endif

Engineering Best Practices & Common Pitfalls

  1. Use std::polymorphic<T> for Dynamic Class Hierarchies with Value Semantics:

    • Replace standard smart pointer containers (std::vector<std::unique_ptr<Base>>) with std::vector<std::polymorphic<Base>> whenever the domain calls for copyable, composable value types.
  2. Ensure Base Class Destructors are Virtual:

    • std::polymorphic<T> relies on polymorphic destruction semantics. Always declare a virtual ~T() = default; virtual destructor in base classes to guarantee defined behavior during deallocation.
  3. Check valueless_after_move() Before Accessing Moved-From Objects:

    • Moving a std::polymorphic<T> leaves the source object in a valueless state. Dereferencing (* or ->) a valueless object invokes undefined behavior. Always verify .valueless_after_move() if re-using moved variables.
  4. Choose std::polymorphic<T> vs. std::indirect<T> Correctly:

    • Use std::polymorphic<T> for dynamic hierarchies involving virtual inheritance and subtyping polymorphism.
    • Use std::indirect<T> (its sibling C++26 type) for single non-polymorphic types, such as implementation hiding (Pimpl idiom) or recursive struct definitions (e.g., binary tree nodes).

Relationships

  • Related to:
  • Superseded by: None.
  • Supersedes: Manual virtual clone() method patterns and std::unique_ptr<Base> wrappers for value-based dynamic hierarchies.

Back to C++26 section.