P0201R6
polymorphic_value: A Polymorphic Value Type for C++

Published Proposal,

This version:
https://wg21.link/P0201
Issue Tracking:
GitHub
Authors:
Audience:
LEWG
Project:
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
Source:
https://github.com/jbcoe/polymorphic_value/blob/main/documentation/p0201.md

1. Change history

Changes in P0201R6

Changes in P0201R5

Changes in P0201R4

Changes in P0201R3

Changes in P0201R2

Changes in P0201R1

2. TL;DR

Add a class template, polymorphic_value<T>, to the standard library to support polymorphic objects with value-like semantics.

3. Introduction

The class template, polymorphic_value, confers value-like semantics on a free-store allocated object. A polymorphic_value<T> may hold an object of a class publicly derived from T, and copying the polymorphic_value<T> will copy the object of the derived type.

3.1. Motivation: Composite objects

Use of components in the design of object-oriented class hierarchies can aid modular design as components can be potentially re-used as building-blocks for other composite classes.

We can write a simple composite object formed from two components as follows:

// Simple composite
class CompositeObject_1 {
  Component1 c1_;
  Component2 c2_;

  public:
  CompositeObject_1(const Component1& c1,
                    const Component2& c2) :
                    c1_(c1), c2_(c2) {}

  void foo() { c1_.foo(); }
  void bar() { c2_.bar(); }
};

The composite object can be made more flexible by storing pointers to objects allowing it to take derived components in its constructor. (We store pointers to the components rather than references so that we can take ownership of them).

// Non-copyable composite with polymorphic components (BAD)
class CompositeObject_2 {
  IComponent1* c1_;
  IComponent2* c2_;

  public:
  CompositeObject_2(IComponent1* c1,
                    IComponent2* c2) :
                    c1_(c1), c2_(c2) {}

  void foo() { c1_->foo(); }
  void bar() { c2_->bar(); }

  CompositeObject_2(const CompositeObject_2&) = delete;
  CompositeObject_2& operator=(const CompositeObject_2&) = delete;

  CompositeObject_2(CompositeObject_2&& o) : c1_(o.c1_), c2_(o.c2_) {
    o.c1_ = nullptr;
    o.c2_ = nullptr;
  }

  CompositeObject_2& operator=(CompositeObject_2&& o) {
    delete c1_;
    delete c2_;
    c1_ = o.c1_;
    c2_ = o.c2_;
    o.c1_ = nullptr;
    o.c2_ = nullptr;
  }

  ~CompositeObject_2()
  {
    delete c1_;
    delete c2_;
  }
};

CompositeObject_2's constructor API is unclear without knowing that the class takes ownership of the objects. We are forced to explicitly suppress the compiler-generated copy constructor and copy assignment operator to avoid double-deletion of the components c1_ and c2_. We also need to write a move constructor and move assignment operator.

Using unique_ptr makes ownership clear and saves us writing or deleting compiler generated functions:

// Non-copyable composite with polymorphic components
class CompositeObject_3 {
  std::unique_ptr<IComponent1> c1_;
  std::unique_ptr<IComponent2> c2_;

  public:
  CompositeObject_3(std::unique_ptr<IComponent1> c1,
                    std::unique_ptr<IComponent2> c2) :
                    c1_(std::move(c1)), c2_(std::move(c2)) {}

  void foo() { c1_->foo(); }
  void bar() { c2_->bar(); }
};

The design of CompositeObject_3 is good unless we want to copy the object.

We can avoid having to define our own copy constructor by using shared pointers. As shared-ptr's copy constructor is shallow, we need to modify the component pointers to be pointers-to const to avoid introducing shared mutable state [S.Parent].

// Copyable composite with immutable polymorphic components class
class CompositeObject_4 {
  std::shared_ptr<const IComponent1> c1_;
  std::shared_ptr<const IComponent2> c2_;

  public:
  CompositeObject_4(std::shared_ptr<const IComponent1> c1,
                    std::shared_ptr<const IComponent2> c2) :
                    c1_(std::move(c1)), c2_(std::move(c2)) {}

  void foo() { c1_->foo(); }
  void bar() { c2_->bar(); }
};

CompositeObject_4 has polymorphism and compiler-generated destructor, copy, move and assignment operators. As long as the components are not mutated, this design is good. If non-const functions of components are used then this won’t compile.

Using polymorphic_value a copyable composite object with polymorphic components can be written as:

// Copyable composite with mutable polymorphic components
class CompositeObject_5 {
  std::polymorphic_value<IComponent1> c1_;
  std::polymorphic_value<IComponent2> c2_;

  public:
  CompositeObject_5(std::polymorphic_value<IComponent1> c1,
                    std::polymorphic_value<IComponent2> c2) :
                    c1_(std::move(c1)), c2_(std::move(c2)) {}

  void foo() { c1_->foo(); }
  void bar() { c2_->bar(); }
};

The component c1_ can be constructed from an instance of any class that inherits from IComponent1. Similarly, c2_ can be constructed from an instance of any class that inherits from IComponent2.

CompositeObject_5 has a compiler-generated destructor, copy constructor, move constructor, assignment operator and move assignment operator. All of these compiler-generated functions will behave correctly.

3.2. Deep copies

To allow correct copying of polymorphic objects, polymorphic_value uses the copy constructor of the owned derived-type object when copying a base type polymorphic_value. Similarly, to allow correct destruction of polymorphic component objects, polymorphic_value uses the destructor of the owned derived-type object in the destructor of a base type polymorphic_value.

The requirements of deep-copying can be illustrated by some simple test code:

// GIVEN base and derived classes.
class Base { virtual void foo() const = 0; };
class Derived : public Base { void foo() const override {} };

// WHEN a polymorphic_value to base is formed from a derived object
polymorphic_value<Base> poly(Derived());
// AND the polymorphic_value to base is copied.
auto poly_copy = poly;

// THEN the copy owns a distinct object
assert(&*poly != &*poly_copy);
// AND the copy owns a derived type.
assert(dynamic_cast<Derived*>(*&poly_copy));

Note that while deep-destruction of a derived class object from a base class pointer can be performed with a virtual destructor, the same is not true for deep-copying. C++ has no concept of a virtual copy constructor and we are not proposing its addition. The class template shared_ptr already implements deep-destruction without needing virtual destructors; deep-destruction and deep-copying can be implemented using type-erasure [Impl].

3.3. Pointer constructor

polymorphic_value can be constructed from a pointer and optionally a copier and/or deleter. The polymorphic_value constructed in this manner takes ownership of the pointer. This constructor is potentially dangerous as a mismatch in the dynamic and static type of the pointer will result in incorrectly synthesized copiers and deleters, potentially resulting in slicing when copying and incomplete deletion during destruction.

class Base { /* functions and members */ };
class Derived : public Base { /* functions and members */ };

Derived* d = new Derived();
Base* p = d; // static type and dynamic type differ
polymorphic_value<Base> poly(p);

// This copy will have been made using Base’s copy constructor.
polymorphic_value<Base> poly_copy = poly;

// Destruction of poly and poly_copy uses Base’s destructor.

While this is potentially error prone, we have elected to trust users with the tools they are given. shared_ptr and unique_ptr have similar constructors and issues. There are more constructors for polymorphic_value of a less expert-friendly nature that do not present such dangers including a factory function make_polymorphic_value.

Static analysis tools can be written to find cases where static and dynamic types for pointers passed in to polymorphic_value constructors are not provably identical.

If the user has not supplied a custom copier or deleter, an exception bad_polymorphic_value_construction is thrown from the pointer-constructor if the dynamic and static types of the pointer argument do not agree. In cases where the user has supplied a custom copier and deleter it is assumed that they will do so to avoid slicing and incomplete destruction: a class heirarchy with a custom Clone function and virtual destructor would make use of Clone in a user-supplied copier.

3.4. Empty state

polymorphic_value presents an empty state as it is desirable for it to be cheaply constructed and then later assigned. In addition, it may not be possible to construct the T of a polymorphic_value<T> if it is an abstract class (a common intended use pattern). While permitting an empty state will necessitate occasional checks for null, polymorphic_value is intended to replace the uses of pointers or smart pointers where such checks are also necessary. The benefits of default constructability (use in vectors and maps) outweigh the costs of a possible empty state.

A nullable polymorphic_value<T> could be mimicked with std::optional<polymorphic_value<T>> but this is verbose and would require partial template specialization of std::optional to avoid the overhead that would be incurred by a nullable polymorphic_value. But perhaps the most unfortunate side effect of this approach is that to access the underlying T the std::optional must be dereferenced to get to the polymorphic_value, which then requires a second dereference to get to the underlying T. This access pattern is undesirable.

3.5. Lack of hashing and comparisons

For a given user-defined type, T, there are multiple strategies to make polymorphic_value<T> hashable and comparable. Without requiring additional named member functions on the type, T, or mandating that T has virtual functions and RTTI, the authors do not see how polymorphic_value can generically support hashing or comparisons. Incurring a cost for functionality that is not required goes against the 'pay for what you use' philosophy of C++.

For a given user-defined type T the user is free to specialize std::hash and implement comparison operators for polymorphic_value<T>.

3.6. Custom copiers and deleters

The resource management performed by polymorphic_value - copying and destruction of the managed object - can be customized by supplying a _copier_ and _deleter_. If no copier or deleter is supplied then a default copier or deleter may be used.

A custom copier and deleter are _not_ required, if no custom copier and deleter are provided then the copy constructor and destructor of the managed object will be used.

The default deleter is already defined by the standard library and used by unique_ptr.

We define the default copier in technical specifications below.

3.7. Allocator Support

Previous revisions of this paper did not support allocators. However, support is desirable as it offers an opportunity to mitigate the cost of allocations. There is existing precedence for allocator support of types in the memory management library; std::shared_ptr supports allocators via std::allocate_shared, [M. Knejp] suggested the introduction of std::allocate_unique to support allocator use with std::unique_ptr.

polymorphic_value internally uses type-erased to create an internal control block object responsible for cloning itself and the actual type derived from T. It is possible to provide different types of control blocks to support allocation from multiple sources [Impl].

Memory management for polymorphic_value can be fully controlled via the allocate_polymorphic_value function which allows passing in an allocator to control the source of memory. Custom copier and deleter then use the allocator for allocations and deallocations.

3.8. Design changes from cloned_ptr

The design of polymorphic_value is based upon cloned_ptr (from an early revision of this paper) and modified following advice from LEWG. The authors (who unreservedly agree with the design direction suggested by LEWG) would like to make explicit the cost of these design changes.

polymorphic_value<T> has value-like semantics: copies are deep and const is propagated to the owned object. The first revision of this paper presented cloned_ptr<T> which had mixed pointer/value semantics: copies are deep but const is not propagated to the owned object. polymorphic_value can be built from cloned_ptr and propagate_const but there is no way to remove const propagation from polymorphic_value.

As polymorphic_value is a value, dynamic_pointer_cast, static_pointer_cast and const_pointer_cast are not provided. If a polymorphic_value is constructed with a custom copier or deleter, then there is no way for a user to implement cast operations like those that are provided by the standard for std::shared_ptr.

3.9. No implicit conversions

Following design feedback, polymorphic_value's constructors have been made explicit so that surprising implicit conversions cannot take place. Any conversion to a polymorphic_value must be explicitly requested by user-code.

The converting assignment operators that were present in earlier drafts have also been removed.

For a base class, BaseClass, and derived class, DerivedClass, the converting assignment

polymorphic_value<DerivedClass> derived;
polymorphic_value<Base> base = derived;

is no longer valid, the conversion must be made explicit:

polymorphic_value<DerivedClass> derived;
auto base = polymorphic_value<Base>(derived);

The removal of converting assigments makes make_polymorphic_value slightly more verbose to use:

polymorphic_value<Base> base = make_polymorphic_value<DerivedClass>(args);

is not longer valid and must be written as

auto base = polymorphic_value<Base>(make_polymorphic_value<DerivedClass>(args));

This is somewhat cumbersome so make_polymorphic_value has been modified to take an optional extra template argument allowing users to write

polymorphic_value<Base> base = make_polymorphic_value<Base, DerivedClass>(args);

The change from implicit to explicit construction is deliberately conservative. One can change explicit constructors into implicit constructors without breaking code (other than SFINAE checks), the reverse is not true. Similarly, converting assignments could be added non-disruptively but not so readily removed.

3.10. Impact on the standard

This proposal is a pure library extension. It requires additions to be made to the standard library header <memory>.

4. Technical specifications

Add the following entry to [tab:support.ft]

Macro NameValue|Header(s)| |:-:|:-:|:-:| |__cpp_lib_polymorphic_value|xxxxxxL|<memory>|

4.1. Additions in [memory.syn] 20.2.2:

// [indirect.value], class template indirect_value
template <class T> struct default_copy;
template <class T> struct copier_traits;

template <class T> class polymorphic_value;

template <class T, class ...Ts> 
  constexpr polymorphic_value<T> make_polymorphic_value(Ts&& ...ts); 

template <class T, class A = std::allocator<T>, class... Ts>
  constexpr polymorphic_value<T> allocate_polymorphic_value(A& a, Ts&&... ts); 

template<class T>
  constexpr void swap(polymorphic_value<T>& p, polymorphic_value<T>& u) noexcept;

4.2. X.W Class template copier_traits [copier.traits]

namespace std {
  template <class T>
  struct copier_traits {
    using deleter_type = *see below*;
  };
}
using deleter_type = see below;

4.3. X.X Class template default_copy [default.copy]

namespace std {
template <class T> struct default_copy {
  using deleter_type = default_delete<T>;
  constexpr default_copy() noexcept = default;
  constexpr T* operator()(const T& t) const;
};

} // namespace std

The class template default_copy [p1950r1] serves as the default copier for the class template polymorphic_value.

constexpr T* operator()(const T& t) const;

4.4. X.Y Class bad_polymorphic_value_construction [bad_polymorphic_value_construction]

namespace std {
class bad_polymorphic_value_construction : public exception
{
  public:
    bad_polymorphic_value_construction() noexcept = default;

    const char* what() const noexcept override;
};
}

Objects of type bad_polymorphic_value_construction are thrown to report invalid construction of a polymorphic_value.

const char* what() const noexcept override;

4.5. X.Z Class template polymorphic_value [polymorphic_value]

4.5.1. X.Z.1 Class template polymorphic_value general [polymorphic_value.general]

A polymorphic_value is an object that manages the lifetime of an owned object. A polymorphic_value object may own objects of different types at different points in its lifetime. A polymorphic_value object is empty if it has no owned object. polymorphic_value implements value semantics: the owned object (if any) is copied or destroyed when the polymorphic_value is copied or destroyed. Copying and destruction of the owned object can be customized by supplying a copier and a deleter, respectively.

The template parameter T of polymorphic_value<T> shall be a non-union class type; otherwise the program is ill-formed. The template parameter T of polymorphic_value<T> may be an incomplete type.

A copier and deleter are said to be _present_ if and only if a polymorphic_value object is constructed from a non-null pointer, or from a polymorphic_value object where a copier and a deleter are present.

4.5.2. X.Z.2 Class template polymorphic_value synopsis [polymorphic_value.synopsis]

namespace std {
template <class T> class polymorphic_value {
 public:
  using element_type = T;

  // Constructors
  constexpr polymorphic_value() noexcept;

  constexpr polymorphic_value(nullptr_t) noexcept;

  template <class U> constexpr explicit polymorphic_value(U&& u);

  template <class U, class C=default_copy<U>,
            class D=typename copier_traits<C>::deleter_type>
    constexpr explicit polymorphic_value(U* p, C c=C(), D d=D());

  constexpr polymorphic_value(const polymorphic_value& p);
  
  template <class U>
    constexpr explicit polymorphic_value(const polymorphic_value<U>& p);
  
  constexpr polymorphic_value(polymorphic_value&& p) noexcept;

  template <class U>
    constexpr explicit polymorphic_value(polymorphic_value<U>&& p);

  // Destructor
  constexpr ~polymorphic_value();

  // Assignment
  constexpr polymorphic_value& operator=(const polymorphic_value& p);
  constexpr polymorphic_value& operator=(polymorphic_value&& p) noexcept;

  // Modifiers
  constexpr void swap(polymorphic_value& p) noexcept;

  // Observers
  constexpr const T& operator*() const;
  constexpr T& operator*();
  constexpr const T* operator->() const;
  constexpr T* operator->();
  constexpr explicit operator bool() const noexcept;

  // polymorphic_value specialized algorithms
  friend constexpr void swap(polymorphic_value& p, polymorphic_value& u) noexcept;
};

// polymorphic_value creation
template <class T, class U=T, class... Ts>
constexpr polymorphic_value<T> make_polymorphic_value(Ts&&... ts);

template <class T, class U = T, class A = allocator<U>, class... Ts>
constexpr polymorphic_value<T> allocate_polymorphic_value(allocator_arg_t, A& a, Ts&&... ts);

} // end namespace std

4.5.3. X.Z.3 Class template polymorphic_value constructors [polymorphic_value.ctor]

constexpr polymorphic_value() noexcept;
constexpr polymorphic_value(nullptr_t) noexcept;
template <class U> constexpr explicit polymorphic_value(U&& u);

Let V be remove_cvref_t<U>.

template <class U, class C=default_copy<U>,
          class D=typename copier_traits<C>::deleter_type>
  constexpr explicit polymorphic_value(U* p, C c=C(), D d=D());

If the arguments c and/or d are not supplied, then C and/or D respectively are default constructible types that are not pointer types.

If p is non-null then invoking the expression c(*p) returns a non-null U* is as if copy constructed from *p. The expression d(p) is well formed, has well-defined behavior, and does not throw exceptions. Where q=c(*p), the expression d(q) is well-defined and does not throw exceptions.

template <class U, class A>
constexpr explicit polymorphic_value(U* u, std::allocator_arg_t, const A& alloc)
polymorphic_value(const polymorphic_value& pv);
template <class U> constexpr explicit polymorphic_value(const polymorphic_value<U>& pv);
polymorphic_value(polymorphic_value&& pv) noexcept;
template <class U> constexpr explicit polymorphic_value(polymorphic_value<U>&& pv);

4.5.4. X.Z.4 Class template polymorphic_value destructor [polymorphic_value.dtor]

constexpr ~polymorphic_value();

4.5.5. X.Z.5 Class template polymorphic_value assignment [polymorphic_value.assignment]

constexpr polymorphic_value& operator=(const polymorphic_value& pv);
constexpr polymorphic_value& operator=(polymorphic_value&& pv) noexcept;

4.5.6. X.Z.6 Class template polymorphic_value modifiers [polymorphic_value.modifiers]

constexpr void swap(polymorphic_value& p) noexcept;

4.5.7. X.Z.7 Class template polymorphic_value observers [polymorphic_value.observers]

constexpr const T& operator*() const;
constexpr T& operator*();
constexpr const T* operator->() const;
constexpr T* operator->();
explicit operator bool() const noexcept;

4.5.8. X.Z.8 Class template polymorphic_value creation [polymorphic_value.creation]

template <class T, class U=T, class ...Ts> 
constexpr polymorphic_value<T> make_polymorphic_value(Ts&& ...ts);

[Note: Implementations are encouraged to avoid multiple allocations. - end note]

template <class T, class U = T, class A = allocator<U>, class... Ts>
constexpr polymorphic_value<T> allocate_polymorphic_value(const A& a, Ts&&... ts);

4.5.9. X.Z.9 Class template polymorphic_value specialized algorithms [polymorphic_value.spec]

friend constexpr void swap(polymorphic_value& p, polymorphic_value& u) noexcept;

5. Acknowledgements

The authors would like to thank Maciej Bogus, Matthew Calabrese, Casey Carter, Germán Diago, Louis Dionne, Bengt Gustafsson, Tom Hudson, Stephan T Lavavej, Tomasz Kamiński, David Krauss, Thomas Koeppe, LanguageLawyer, Nevin Liber, Nathan Myers, Roger Orr, Geoff Romer, Patrice Roy, Tony van Eerd and Ville Voutilainen for suggestions and useful discussion.

The authors would also extend thanks for contributions to the reference implementation which has driven the design, including Anthony Williams, Ed Catmur, and Marcell Kiss.

5.1. References

[N3339] "A Preliminary Proposal for a Deep-Copying Smart Pointer", W.E.Brown, 2012

[p1950r1] indirect_value: A Free-Store-Allocated Value Type For C++

[P0302r1] "Removing Allocator support in std::function", Jonathan Wakely

[M. Knejp] P0316R0: allocate_unique and allocator_delete

[S.Parent] "C++ Seasoning", Sean Parent, 2013

[Impl] Reference implementation: polymorphic_value, J.B.Coe

[P0302r1] "Removing Allocator support in std::function", Jonathan Wakely