1. Abstract
Add a class template,indirect_value
, to the C++ Standard Library to support free-store-allocated objects with value-like semantics.
2. Change history
Changes in P1950r1
-
Add design discussion.
-
Add comparison with
andunique_ptr
.polymorphic_value -
Add node-based container and hot-cold splitting as further motivational examples.
-
Add examples of similar classes already in use.
3. Introduction
The class template,indirect_value
, confers value-like semantics on a free-store-allocated object. An indirect_value
may hold an object of a class T, copying the indirect_value
will copy the object T
. When a parent object contains a member of type indirect_value < T >
and
is accessed through a const access path, const
ness will propagate from the parent object to the instance of T
owned by the indirect_value
member.
3.1. Motivation
It may be desirable for a class member to be an incomplete type or for the storage used by a member object to be separate from the class itself. In both such cases, the member would traditionally be represented as pointer:
class MyClass { AType data_member_ ; AnotherType * indirect_data_member_ ; };
The author of such a class will need to implement special member functions as the compiler-generated special member functions will not function correctly (indirect data will not be copied, assigned to or deleted, only the pointer).
Special care will be needed when using the class in multithreaded environments. An instance of
accessed through a const-access-path
will not propagate the
-ness to the indirect data (only to the pointer itself) as pointers do not propagate
to their pointees.
The class template
is a drop-in replacement for pointer members in cases where non-polymorphic data referenced by the
pointer member is logically part of the class. Use of
will ensure that the compiler-generated special member functions
behave correctly and that
is propagated to indirect data.
3.1.1. Node-based containers
A tree or linked list can be implemented as a node-based container. This gives stable references to data in the nodes at the cost of memory indirection when accessing data.
Nodes would typically contain data and pointer(s) to other nodes so that the list or tree can be navigated:
class ForwardListNode { int data_ ; ForwardListNode * next_ ; }; class ForwardList { ForwardListNode * head_ ; public : // ... };
The special member functions of
would need to be user-implemented as the
compiler-generated versions would not copy, move or delete the nodes correctly.
Care must be taken when implementing
-qualified member functions as
will not propagate down
from the
to the
s nor to the data that the nodes contain.
Implementing the ForwardList and ForwardListNode with
corrects these issues:
class ForwardListNode { int data_ ; indirect_value < ForwardListNode > next_ ; }; class ForwardList { indirect_value < ForwardListNode > head_ ; public : // ... };
Compiler-generated special member functions will behave correctly and do not need to be written manually.
-propagation will allow the compiler to ensure that data that is logically part of the container
cannot be modified through a
access path.
3.1.2. Hot-cold splitting
When working with collections of data, CPU caches are often under-utilized when algorithms access certain data members much more frequently than others. This results in some cache lines becoming busier than others, prefetchers preemptively caching memory which is later evicted without being used by the CPU or memory bandwidth becoming a limiting factor in performance. In such cases segregating data structures in terms of hot and cold data can remove pressure on system resources.
class Element { SmallData frequently_accessed_data ; LargeData infrequently_accessed_data ; }; vector < Element > elements ; auto active = find ( elements . begin (), elements . end (), []( const auto & e ) { return e . frequently_accessed_data . active (); };
In such cases adding a level of indirection to the larger, less frequently-accessed data can relieve bandwidth
pressure [C. Ericson]. Using
in this refactoring does not change guarantees around
ness. Compiler-generated special member functions will behave correctly and do not need to be manually implemented.
class Element { SmallData frequently_accessed_data ; indirect_value < LargeData > infrequently_accessed_data ; }; vector < Element > elements ; auto active = find ( elements . begin (), elements . end (), []( const auto & e ) { return e . frequently_accessed_data . active (); };
3.1.3. Pointer to Implementation - PImpl
In C++, when anything in a class definition changes, dependent classes require recompilation. As early as 1992 the
Handle/Body idiom was suggested to break this dependency [J. Coplien]. From this pattern, the PImpl idiom was
specialised [H. Sutter]. Despite its relative maturity, using the PImpl idiom requires careful thought about copying
and
-propagation.
// Header file class widget { public : widget (); ~ widget (); private : class impl ; std :: unique_ptr < impl > pimpl_ ; };
// Implementation file class widget :: impl { // ::: }; widget :: widget () : pimpl_ { std :: make_unique < impl > ( /*...*/ } { } // Destructor needs to be defined in a the same TU as <code data-opaque bs-autolink-syntax='`widget::impl`'>widget::impl</code>. widget ::~ widget () = default ;
For convenience, the widget class will be referred to as the “visible class” and impl class the “
class”. Note, semantically the
class is the implementation details of the visible class.
3.1.3.1. Issues with const-propagation
Usingstd :: unique_ptr
to store the implementation object introduces an issue - within a const
-qualified
member function, an instance of the visible class can mutate data inside the implementation object. This is because const
-qualification applies only to the unique_ptr
value, and not the pointed-to-object.
The compiler is unable to make thread-compatibility assumptions for
objects when
does not propagate:
does not mean immutable in the face of pointer-like-member data.
The desired semantics of a PImpl-owner are value-like, like those of
which has appropriate
and non-
-qualified overloads for
and
.
3.1.3.2. Issues with copies
The copy-constructor and copy-assignment operator ofstd :: unique_ptr
are deleted. Users of a class with a std :: unique_ptr
member will be
required to implement copy-construction and copy-assignment [S. Meyers].
3.1.3.3. An indirect_value
implementation of the PImpl Idiom
Using indirect_value
to implement the PImpl idiom ensures that the PImpl object is const
when accessed through a const
access path and that compiler-generated special member functions will behave correctly and do not need to be manually implemented.
// Header file class widget { public : widget (); widget ( widget && rhs ) noexcept ; widget ( const widget & rhs ); widget & operator = ( widget && rhs ) noexcept ; widget & operator = ( const widget & rhs ); ~ widget (); private : class impl ; std :: indirect_value < impl >> pimpl ; };
// Implementation file class widget :: impl { // ::: }; // Special-member functions need to be defined in a the same TU as <code data-opaque bs-autolink-syntax='`widget::impl`'>widget::impl</code>. widget :: widget ( widget && rhs ) noexcept = default ; widget :: widget ( const widget & rhs ) = default ; widget & widget :: operator = ( widget && rhs ) noexcept = default ; widget & widget :: operator = ( const widget & rhs ) = default ; widget ::~ widget () = default ;
3.2. Design decisions
3.2.1. Should there be be an empty state?
There is a design decision to be made about the default constructor
of
. It could place
in an empty state
with no owned object or it could default construct an owned object
so that
is never empty.
If
is never empty then
must be default
constructible for
to be a regular type.
The default constructor of
would then need to allocate
memory, which cannot be controlled by the user-supplied copier or deleter.
If the default constructed state of
is non-empty then the moved from
state should be similarly non-empty. This requires that
is noexcept move constructible.
Allowing an empty state for
imposes preconditions on operator* and operator->
(similar to those of the raw pointer it may be replacing) but removes requirements on
and the need
to allocate memory.
A nullable
could be mimicked with
but this is verbose,
does not solve the problem of
needing to be noexcept move-constructible and would require
partial template specialization of
to avoid overhead that would not be incurred by a
nullable
.
As designed,
has an empty state where it owns no object.
A default constructed
is empty and the empty state is the state of a moved-from object.
3.2.2. Is there a small object optimization?
A small object optimization is permissible for
.
A small buffer that is part of the
class can be used to store the owned object
if it is small. We do not require that
is the same size as a raw pointer;
implementers are free, but not required, to add a small buffer optimization as a quality of
implementation improvement.
It would be possible for the size of the small buffer to be specified as a template argument.
Such a design would be inconsistent with the broader Standard Library design:
,
,
all permit a small object optimization but do not
let users specify the size of the buffer.
3.2.3. How are allocators supported?
It may be desirable for users to control the way memory is managed by an instance of a class.
Classes such as
and
allow an allocator to be specified as template argument
to control memory management.
Like
,
allows a deleter to be provided as a template argument
so that disposal of the owned object can be customized.
also allows copying to be be
customized by specifying a copier, another template argument.
The in-place constructor of
uses
to create an instance of
with the supplied
constructor argument. This constructor is disabled where the copier and deleter are not
and
.
Memory management for
can be fully controlled by specifying a suitable copier and
deleter and using the pointer constructor
.
3.3. Prior Art
There have been previous proposal for deep-copying smart pointers that proposed copy semantics [W. Brown].cloned_ptr
was proposed in [J. Coe], however under guidance of LEWG this was renamed to polymorphic_value
. With this change in name came the addition of const propagation.
This paper is not unique in these ideas. GitHub code search finds 602k lines of code referencing "PImpl" and 99 C ++ repositories claiming to provide generic implementations of Pimpl. Additionally other authors have addressed this topic [A. Upadyshev]. Some generic implementations of note in the wild are:
Project | Link | Deep Copying | Const Propagation |
---|---|---|---|
Boost Pimpl | https://github.com/sean-/boost-pimpl | Yes | Yes |
pimpl | https://github.com/JonChesterfield/pimpl | Yes | No |
impl_ptr | https://github.com/sth/impl_ptr | Yes | Yes |
Simpl | https://github.com/oliora/samples/blob/master/spimpl.h | Yes | Yes |
smart_pimpl | https://github.com/SBreezyx/smart_pimpl | No | No |
pimpl_on_stack | https://github.com/kuznetsss/pimpl_on_stack/ | Yes | Yes |
deep_const_ptr | https://github.com/torbjoernk/deep_const_ptr | No | Yes |
Divergence on issues such as deep copying and const propagation along with subtleties in implementation mean that a single standardized solution would be of broad benefit to the community of C++ users.
3.4. Completeness of T
Smart pointer types in the Standard Library expect that some of the members can be instantiated with incomplete types [H.Hinnant]. Similarly, this is the case forindirect_value
, the table outlines the expected behaviour for incomplete
pointer types:
Method | Description | Incomplete/Complete |
---|---|---|
| Default constructor | Incomplete |
| Copy-constructor | Complete |
| Move-constructor | Incomplete |
| Destructor | Complete |
| Copy-assignment | Complete |
| Move-assignment | Complete |
| Indirection-operator | Incomplete |
| Indirection-operator | Incomplete |
| Member-of-pointer-operator | Incomplete |
| Member-of-pointer-operator | Incomplete |
| Bool-operator | Incomplete |
| Swap | Incomplete |
3.5. Comparison with unique_ptr < T >
and polymorphic_value < T >
The class template
is in many respects similar to
and the proposed
[J. Coe]. The table below highlights the similarities and differences:
Behaviour |
|
|
|
---|---|---|---|
Default constructed state | Empty | Empty | Empty |
Copyable | Yes | Yes | No |
Const-propagating | Yes | Yes | No |
Polymorphic | No | Yes | Yes |
Customizable memory access | Yes | No | Yes |
3.6. 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
4.1. X.X Class template default_copy
[default.copy]
The class template default_copy serves as the default copier for the class templatenamespace std { template < class T > struct default_copy { T * operator ()( const T & t ) const ; }; } // namespace std
indirect_value
.
The template parameter T
of default_copy
may be an incomplete type.
-
Returns:
new T ( t );
4.2. X.Y Class template indirect_value
[indirect_value]
4.2.1. X.Y.1 Class template indirect_value
general [indirect_value.general]
An indirect_value
is an object that owns another object and manages that other object through a pointer. More precisely, an indirect value is an object v
that stores a pointer to a second object p
and will dispose of p
when v
is itself destroyed (e.g., when leaving block scope (9.7)). In this context, v
is said to own p
.
An
object is empty if it does not own a pointer.
Copying a non-empty
will copy the owned object so that the copied
will have its own unique copy of the owned object.
Copying from an empty
produces another empty
.
Copying and disposal of the owned object can be customised by supplying a copier and deleter.
The template parameter
of
must be a non-union class type.
The template parameter
of
may be an incomplete type.
[Note: Implementations are encouraged to avoid the use of dynamic memory for ownership of small objects.]
4.2.2. X.Y.2 Class template indirect_value
synopsis [indirect_value.synopsis]
template < class T , class C = std :: default_copy < T > , class D = std :: default_delete < T >> class indirect_value { public : using value_type = T ; // Constructors constexpr indirect_value () noexcept ; explicit indirect_value ( T * p , C c = C {}, D d = D {}); indirect_value ( const indirect_value & p ); indirect_value ( indirect_value && p ) noexcept ; template < class ...Ts > indirect_value ( std :: in_place_t , Ts && ... ts ); // See below // Destructor ~ indirect_value (); // Assignment indirect_value & operator = ( const indirect_value & p ); indirect_value & operator = ( indirect_value && p ) noexcept ; // Modifiers void swap ( indirect_value < T >& p ) noexcept ; // Observers T & operator * (); T * operator -> (); const T & operator * () const ; const T * operator -> () const ; explicit operator bool () const noexcept ; }; // indirect_value creation template < class T , class ...Ts > indirect_value < T > make_indirect_value ( Ts && ... ts ); // See below // indirect_value specialized algorithms template < class T > void swap ( indirect_value < T >& p , indirect_value < T >& u ) noexcept ; } // end namespace std
4.2.3. X.Y.3 Class template indirect_value
constructors [indirect_value.ctor]
-
Remarks: The method shall work with incomplete pointer type for
.T -
Effects: Constructs an empty
.indirect_value -
Postconditions:
.bool ( * this ) == false
-
-
Effects: Creates an
object that owns the pointerindirect_value
. Ifp
is non-null then the copier and deleter of thep
constructed are moved fromindirect_value
andc
.d -
Requires:
andC
satisfy the requirements of CopyConstructible. IfD
is non-null then the expressionp
returns an object of typec ( * p )
. The expressionT *
is well-formed, has well-defined behaviour, and does not throw exceptions.d ( p ) -
Postconditions:
.bool ( * this ) == bool ( p ) -
Remarks: A custom copier and deleter are said to be ‘present’ in a
initialised with this constructor.indirect_value
-
Constraints:
is_copy_constructible_v < T > -
Effects: Creates a
object that owns a copy of the object managed byindirect_value
. The copy is created by the copier inp
. Ifp
has a custom copier and deleter then the custom copier and deleter of thep
constructed are copied from those inindirect_value
.p -
Throws: Any exception thrown by the copier or
if required storage cannot be obtained.bad_alloc -
Postconditions:
.bool ( * this ) == bool ( p )
-
Effects: Move-constructs an
instance fromindirect_value
. Ifp
has a custom copier and deleter then the copier and deleter of thep
constructed are the same as those inindirect_value
.p -
Postconditions:
contains the old value of* this
.p
is empty.p -
Remarks: The method shall work with incomplete pointer type for
.T
-
Effects: Constructs an
which owns an object of typeindirect_value
direct-non-list-initialized withT std :: forward < Ts > ( ts )... -
Throws: Any exception thrown by the selected constructor of
orT
if required storage cannot be obtained.bad_alloc -
Requires
.is_same_v < C , default_copy > && is_same_v < D , default_delete >
4.2.4. X.Y.4 Class template indirect_value
destructor [indirect_value.dtor]
~ indirect_value ();
-
Effects: If
there are no effects. If a custom deleterget () == nullptr
is present thend
is called and the copier and deleter are destroyed. Otherwise the destructor of the managed object is called.d ( p )
4.2.5. X.Y.5 Class template indirect_value
assignment [indirect_value.assignment]
indirect_value & operator = ( const indirect_value & p );
-
Constraints:
is_copy_assignable < T > -
Effects:
owns a copy of the resource managed by* this
. Ifp
has a custom copier and deleter then the copy is created by the copier inp
, and the copier and deleter ofp
are copied from those in* this
. Otherwise, the resource managed byp
is initialised by the copy constructor of the resource managed by* this
.p -
Throws: Any exception thrown by the copier or
if required storage cannot be obtained.bad_alloc -
Returns:
.* this -
Postconditions:
.bool ( * this ) == bool ( p )
-
Effects: Ownership of the resource managed by
is transferred to this. Ifp
has a custom copier and deleter then the copier and deleter ofp
is the same as those in* this
.p -
Returns:
.* this -
Postconditions:
contains the old value of* this
.p
is empty.p
4.2.6. X.Y.6 Class template indirect_value
modifiers [indirect_value.modifiers]
void swap ( indirect_value & p ) noexcept ;
-
Effects: Exchanges the contents of
andp
.* this -
Remarks: The method shall work with incomplete pointer type for
.T
4.2.7. X.Y.7 Class template indirect_value
observers [indirect_value.observers]
T & operator * (); const T & operator * () const ;
-
Requires:
.bool ( * this ) -
Returns: A reference to the owned object.
T * operator -> () noexcept ; const T * operator -> () const noexcept ;
-
Requires:
.bool ( * this ) -
Returns: A pointer to the owned object.
-
Remarks: The method shall work with incomplete pointer type for
.T
-
Returns: false if the
is empty, otherwise true.indirect_value -
Remarks: The method shall work with incomplete pointer type for
.T
4.2.8. X.Z.8 Class template indirect_value
creation [indirect_value.creation]
template < class T , class U = T , class ...Ts > indirect_value < T > make_indirect_value ( Ts && ... ts );
-
Constraints:
is true.is_constructible_v < U , Ts ... > -
Expects:
meets theU
requirements.Cpp17CopyConstructible -
Returns: A
owning an object of type direct-non-list-initialized withindirect_value < T >
.std :: forward < Ts > ( ts )... -
Requires
.is_same_v < C , default_copy > && is_same_v < D , default_delete >
5. Acknowledgements
The authors would like to thank Thomas Russell, and Andrew Bennieston for useful discussions on the topic and the BSI panel for on-going support.6. References
[J. Coe] p0201r3: A polymorphic value-type for C++
[J. Coplien] Advanced C++ Programming Styles and Idioms (Addison-Wesley), James O. Coplien, 1992
[C. Ericson] Memory Optimization, Christer Ericson, Games Developers Conference, 2003
[A. Upadyshev] PIMPL, Rule of Zero and Scott Meyers, Andrey Upadyshev, 2015
[H. Hinnant] “Incomplete types and shared_ptr / unique_ptr”, Howard Hinnant, 2011
[H. Sutter] "Pimpls - Beauty Marks You Can Depend On", Herb Sutter, 1998
[Impl] Reference implementation: indirect_value, J.B.Coe
[S. Meyers] Effective Modern C++, Item 22: When using the Pimpl Idiom, define special member functions in the implementation file, Scott Meyers, 2014
[W. Brown] n3339: A Preliminary Proposal for a Deep-Copying Smart Pointer, Walter E. Brown, 2012