1. Acknowledgements
I’d like to thank the following:
-
Jackie Kay and Brittany Friedman for encouraging me to submit this proposal.
-
Gor Nishanov, whose coroutine slides at CppCon 2016 gave me the final push to sit down and write this.
-
Bryce "Wash" Lelbach for representing this paper when I was unable to.
-
Mark Zeren for constantly bugging me to see if I might update this paper.
-
JF Bastien and Billy O’Neal for assisting me with some atomic operations, which I am absolute garbage at.
2. Revision History
2.1. Revision 1
-
Rewrote proposal in bikeshed, the hot new format that every cool proposal writer is using now.
-
Clarified section on addressof operator, now that
is being submittedout_ptr -
Removed
as it was removed fromretain_ptr :: unique for C++20.shared_ptr -
Removed code example of implementing a
-like type.std :: future -
Changed
toretain_ptr :: detach retain_ptr :: release -
Changed name of
toretain_t retain_object_t -
Changed atomicity of
based on suggestions from Billy O’Neal.atomic_reference_count -
Changed wording to use C++17/C++20 features.
-
Changed order of document. §7 Technical Specification is now at the very end of the document, and §6 Examples are further up.
-
Added
andadopt_object_t to let users decide what action they want on construction and reset.default_action -
Added
,static_pointer_cast ,dynamic_pointer_cast , andconst_pointer_cast .reinterpret_pointer_cast -
Added deduction guides to bring
in line with other smart pointersretain_ptr -
Added more code examples
2.2. Revision 0
Initial Release 🎉
3. Motivation
There are a wide variety of C and C++ APIs that rely on reference counting,
but either because of the language (C) or the age of the library (C++), they
are unable to be safely use with either or . In the former case, there is no guaranteed way to make
sure the is the last instance (i.e., that it is unique), and in
the latter case, manages its own API. In addition, existing
intrusive smart pointers such as , Microsoft’s , or WebKit’s aptly named do not meet the needs of modern C++ smart pointers or APIs.
This proposal attempts to solve these shortcomings in an extensible and future
proof manner, while permitting a simple upgrade path for existing project
specific intrusive smart pointers and providing the opportunity for value
semantics when interfacing with opaque interfaces.
Those that work on systems or tools that rely on reference counting stand to
benefit most from . Additionally, would be a benefit
to standard library implementers for types that secretly use intrusive
reference counting such as a coroutines capable future and promise.
If is added to the standard library, the C++ community would also
be one step closer to a non-atomic and , much like
Rust’s .
A reference implementation of , along with examples of its use,
can be found on github.
4. Scope and Impact
would ideally be available in the standard
header. It is a pure extension to the C++ standard library and can (more or
less) be implemented using any conforming C++14 or C++11 compiler with very
little effort. See the §7 Technical Specification for interface and behavior
details.
5. Frequently Asked Questions
Several common questions regarding the design of can be
found below.
5.1. How does intrusive_ptr not meet the needs of modern C++?
has had nearly the same interface since its introduction
in 2001 by Peter Dimov. Furthermore, has several
failings in its API that cannot be changed from without breaking compatibility.
When constructing a , by default it increments the
reference count. This is because of its mixin, which
starts with a reference count of 0 when it is default constructed. Out of all
the libraries I tried to look at, this was the one instance where an object
required it be incremented after construction. This proposal rectifies this
by permitting each traits instance to choose its default action during
construction.
Additionally, does not have the ability to "overload"
its type member, requiring some additional work when interfacing with
C APIs (e.g., ). This
also precludes it from working with types that meet the NullablePointer named requirements (a feature that supports).
Furthermore, relies on ADL calls of two functions:
-
intrusive_add_ref -
intrusive_release
While this approach is fine in most cases, it does remove the ability to easily "swap out" the actions taken when increment or decrementing the reference count (e.g., setting a debugger breakpoint on a specific traits implementation, but not on all traits implementations). This naming convention uses terms found in Microsoft’s COM. While this isn’t an issue per se, it would be odd to have functions with those names found in the standard.
5.2. Why is it called retain_ptr and not intrusive_ptr ?
diverges from the design of . It was decided
to change the name so as to not cause assumptions of 's interface
and behavior.
Some additional names that might be considered (for bikeshedding) are:
-
extend_ptr -
counted_ptr -
borrow_ptr -
mutual_ptr -
joint_ptr
Comedy Option:
-
auto_ptr
5.3. Is retain_ptr atomic?
is only atomic in its reference count increment and decrements
if the object it manages is itself atomic in its reference count operations.
5.4. Does retain_ptr support allocators?
itself does not support allocators, however the object whose
lifetime it extends can.
5.5. Can retain_ptr be constexpr?
Possibly. However, I question the usefulness for a constexpr capable intrusive
smart pointer, as most use cases are intended for non-constexpr capable
interfaces such as incomplete types and polymorphic classes. Additionally, allows one to utilize value semantics on C and C++ APIs. If this
is desired in a constexpr context, one can simply use constexpr values (i.e.,
reference counting is not a necessary aspect of constexpr)
5.6. Does retain_ptr adopt or retain an object on construction?
The default action that takes on construction or reset is
determined by the for the . If the traits type
has a member named , the will use that to delegate
to the correct constructor. If there is no type alias member named , the default operation is to adopt the object (i.e., it
does not increment the reference count during its construction). The type alias must be either or .
5.7. Why provide retain_object_t and adopt_object_t ?
and act as sentinel types to provide
explicit requests to either extend or adopt an object when constructing
or resetting a . This mostly comes into play when interacting
with APIs that return a borrowed (rather than a new) reference to an object
without increment its reference count.
Technically, an would be possible.
However, this would be the first time such an API is placed into the standard
library. Using a boolean parameter, as found in is
unsatisfactory and does not help describe the operation the user is requesting.
The names of these sentinels are available for bikeshedding. Some other
possible names for include:
-
retain_element_t -
extend_element_t -
retainobj_t -
extendobj_t
While names include:
-
borrow_object_t -
borrow_element_t -
borrowobj_t -
adopt_element_t -
adoptobj_t
5.8. Does retain_ptr overload the addressof operator?
Originally, this proposal suggested might in some small edge case
require an overload for the addressof . This was, with absolutely
no surprise, contentious and asked to be removed. However, Microsoft’s overloads the addressof operator for initializing it via C APIs
(i.e., APIs which initialize a pointer to a pointer). Since then, JeanHyde
Meneide’s out_ptr proposal [P1132] was written and thus solves this slight
issue.
5.9. Can retain_traits store state?
No. Any important state regarding the object or how it is retained, can be
stored in the object itself. For example, if the reference count needs to be
external from the object, would be a better choice.
5.10. Why not just wrap a unique_ptr with a custom deleter?
This is an extraordinary amount of code across many existing libraries that
would not be guaranteed to have a homogenous interface. For example, using with an OpenCL context object (without checking for errors in both
implementations) is as simple as:
struct context_traits { using pointer = cl_context ; static void increment ( pointer p ) { clRetainContext ( p ); } static void decrement ( pointer p ) { clReleaseContext ( p ); } }; struct context { using handle_type = retain_ptr < cl_context , context_traits > ; using pointer = handle_type :: pointer ; context ( pointer p , retain_object_t ) : handle ( p , retain ) { } context ( pointer p ) : handle ( p ) { } private : handle_type handle ; };
Using the approach requires more effort. In this case, it is
twice as long to get the same functionality.
struct context_deleter { using pointer = cl_context ; void increment ( pointer p ) const { if ( p ) { clRetainContext ( p ); } // retain_ptr checks for null for us } void operator () ( pointer p ) const { clReleaseContext ( p ); } }; struct retain_object_t { }; constexpr retain_object_t retain { }; struct context { using handle_type = unique_ptr < cl_context , context_deleter > ; using pointer = handle_type :: pointer ; context ( pointer p , retain_object_t ) : context ( p ) { handle . get_deleter (). increment ( handle . get ()); } context ( pointer p ) : handle ( p ) { } context ( context const & that ) : handle ( that . handle . get ()) { handle . get_deleter (). increment ( handle . get ()) } context & operator = ( context const & that ) { context ( that . handle . get (), retain ). swap ( * this ); return * this ; } void swap ( context & that ) noexcept { handle . swap ( that ); } private : handle_type handle ; };
As we can see, using saves effort, allowing us in most cases to
simply rely on the "rule of zero" for constructor management. It will also not
confuse/terrify maintainers of code bases where objects construct a with the raw pointer of another (and unique ownership is not
transferred).
6. Examples
Some C APIs that would benefit from are
-
OpenCL
-
Mono (Assembly and Image types)
-
CPython
-
ObjC Runtime
-
Grand Central Dispatch
Inside the [implementation] repository is an extremely basic example of using with Python.
6.1. OpenCL
As shown above, using with OpenCL is extremely simple.
struct context_traits { using pointer = cl_context ; static void increment ( pointer p ) { clRetainContext ( p ); } static void decrement ( pointer p ) { clReleaseContext ( p ); } }; struct context { using handle_type = retain_ptr < cl_context , context_traits > ; using pointer = handle_type :: pointer ; context ( pointer p , retain_object_t ) : handle ( p , retain ) { } context ( pointer p ) : handle ( p ) { } private : handle_type handle ; };
6.2. ObjC Runtime
Additionally, while some additional work is needed to interact with the rest of
the ObjC runtime, can be used to simulate ARC and remove its
need entirely when writing ObjC. This means that one could, theoretically,
write ObjC and ObjC++ capable code without having to actually write ObjC or
ObjC++.
struct objc_traits { using pointer = CFTypeRef ; static void increment ( pointer p ) { CFRetain ( p ); } static void decrement ( pointer p ) { CFRelease ( p ); } };
6.3. DirectX and COM
Because DirectX is a COM interface, it can also benefit from the use of by simply using the same common traits interface used for all COM
objects. The following code is slightly adapted from Microsoft’s GpuResource
class in the DirectX Graphics Samples. The current form of the code uses the
Microsoft provided , however the point here is to show how can work as a drop-in replacement for this type.
struct com_traits { static void increment ( IUnknown * ptr ) { ptr -> AddRef (); } static void decrement ( IUnknown * ptr ) { ptr -> Release (); } }; template < class T > using com_ptr = retain_ptr < T * , com_traits > ; struct GpuResource { friend class GraphicsContext ; friend class CommandContext ; friend class ComputeContext ; GpuResource ( ID3D12Resource * resource , D3D12_RESOURCE_STATES current ) : resource ( resource ), usage_state ( current ) { } GpuResource () = default ; ID3D12Resource * operator -> () noexcept { return this -> resource . get (); } ID3D12Resource const * operator -> () const noexcept { return this -> resource . get (); } protected : com_ptr < ID3D12Resource > resource { }; D3D12_RESOURCE_STATES usage_state = D3D12_RESOURCE_STATE_COMMON ; D3D12_RESOURCE_STATES transitioning_state = D3D_RESOURCE_STATES ( - 1 ); D3D12_GPU_VIRTUAL_ADDRESS virtual_address = D3D12_GPU_VIRTUAL_ADDRESS_NULL ; void * user_memory = nullptr ; };
6.4. WebKit’s RefPtr
As a small demonstration of replacing existing intrusive smart pointers with the author presents the following code from WebKit that uses the
existing type, followed by the same code expressed with .
This is not meant to be a fully functionioning code sample, but rather what the
effects of a refactor to using might result in
RefPtr < Node > node = adoptRef ( rawNodePointer );
auto node = retain_ptr < Node > ( rawNodePointer , adopt_object );
7. Technical Specification
A retain pointer is an object that extends the lifetime of another object
(which in turn manages its own dispostion) and manages that other object
through a pointer. Specifically, a retain pointer is an object r that stores
a pointer to a second object p and will cease to extend the lifetime of p when r is itself destroyed (e.g., when leaving a block scope). In this
context, r is said to retain , and p is said to be a self disposing
object.
When p's lifetime has reached its end, p will dispose of itself as it sees fit. The conditions regarding p's lifetime is handled by some count c that p comprehends, but is otherwise not directly accessible to r.
The mechanism by which r retains and manages the lifetime of p is known as p's associated retainer, a stateless object that provides mechanisms for r to increment, decrement, and (optionally) provide access to c. In this
context, r is able to increment , decrement , or access the c of p.
Let the notation r.p denote the pointer stored by r. Upon request, r can explicitly choose to increment c when r.p is replaced.
Additionally, r can, upon request, transfer ownership to another retain pointer r2. Upon completion of such a transfer, the following postconditions hold:
-
r2.p is equal to the pre-transfer r.p, and
-
r.p is equal to
nullptr
Furthermore, r can, upon request, extend ownership to another retain pointer r2. Upon completion of such an extension, the following postconditions hold:
-
r2.p is equal to r.p
-
c has been incremented by 1
Each object of a type instantiated from the template
specified in this proposal has the lifetime extension semantics specified
above of a retain pointer. In partical satisfaction of these semantics, each
such is , , and . The template parameter of may be
an incomplete type. (Note: The uses of include providing
exception safety for self disposing objects, extending management of self
disposing objects to a function, and returning self disposing objects from a
function.)
class atomic_reference_count < T > ; class reference_count < T > ; class retain_object_t ; class adopt_object_t ; template < class T > struct retain_traits ; template < class T , class R = retain_traits < T >> class retain_ptr ; template < class T , class R > void swap ( retain_ptr < T , R >& x , retain_ptr < T , R >& y ) noexcept ; template < class T , class R , class U > retain_ptr < T , R > dynamic_pointer_cast ( retain_ptr < U , R > const & ) noexcept ; template < class T , class R , class U > retain_ptr < T , R > static_pointer_cast ( retain_ptr < U , R > const & ) noexcept ; template < class T , class R , class U > retain_ptr < T , R > const_pointer_cast ( retain_ptr < U , R > const & ) noexcept ; template < class T , class R , class U > retain_ptr < T , R > reinterpret_pointer_cast ( retain_ptr < U , R > const & ) noexcept ; template < class T , class R , class S = R > strong_ordering operator <=> ( retain_ptr < T , R > const & x , retain_ptr < T , S > const & y ) noexcept ; template < class T , class R > strong_ordering operator <=> ( retain_ptr < T , R > const & x , nullptr_t ) noexcept ; template < class T , class R > strong_ordering operator <=> ( nullptr_t , retain_ptr < T , R > const & y ) noexcept ;
7.1. atomic_reference_count < T > and reference_count < T >
and are mixin types,
provided for user defined types that simply rely on and to
have their lifetime extended by . The template parameter is
intended to be the type deriving from or (a.k.a. the curiously repeating template pattern, CRTP).
template < class T > struct atomic_reference_count { friend retain_traits < T > ; protected : atomic_reference_count () = default ; private : atomic < uint_least64_t > count { 1 }; // provided for exposition }; template < class T > struct reference_count { friend retain_traits < T > ; protected : reference_count () = default ; private : uint_least64_t count { 1 }; // provided for exposition };
7.2. retain_object_t and adopt_object_t
and are sentinel types, with constexpr
instances and respectively.
namespace std { struct retain_object_t { constexpr retain_object_t () = default ; }; struct adopt_object_t { constexpr adopt_object_t () = default ; }; constexpr retain_object_t retain_object { }; constexpr adopt_object_t adopt_object { }; }
7.3. retain_traits < T >
The class template serves the default traits object for the
class template . Unless is specialized for a
specific type, the template parameter must inherit from either or . In the event that is specialized for a type, the template parameter may be
an incomplete type.
namespace std { template < class T > struct retain_traits { static void increment ( atomic_reference_count < T >* ) noexcept ; static void decrement ( atomic_reference_count < T >* ) noexcept ; static long use_count ( atomic_reference_count < T >* ) noexcept ; static void increment ( reference_count < T >* ) noexcept ; static void decrement ( reference_count < T >* ) noexcept ; static long use_count ( reference_count < T >* ) noexcept ; }; }
static void increment ( atomic_reference_count < T >* ptr ) noexcept ;
1 Effects: Increments the internal reference count for ptr withmemory_order :: relaxed
2 Postcondition:has been incremented by 1.ptr -> count
static void decrement ( atomic_reference_count < T >* ptr ) noexcept ;
1 Effects: Decrements the internal reference count for ptr with. If the internal reference count of ptr reaches 0, it is disposed of viamemory_order :: acq_rel .delete
static long use_count ( atomic_reference_count < T >* ptr ) noexcept ;
1 Returns: The internal reference count for ptr with.memory_order :: acquire
static void increment ( reference_count < T >* ptr ) noexcept ;
1 Effects: Increments the internal reference count for ptr by 1.
static void decrement ( reference_count < T >* ptr ) noexcept ;
1 Effects: Decrements the internal reference count for ptr by 1. If the count reaches 0, ptr is disposed of via.delete
static long use_count ( reference_count < T >* ptr ) noexcept ;
1 Returns: The reference count for ptr.
7.4. retain_ptr < T >
The default type for the template parameter is . A
client supplied template argument shall be an object with non-member
functions for which, given a of type , the
expressions and are valid and has the
effect of retaining or disposing of the pointer as appropriate for that
retainer.
If the qualified-id is valid and denotes a type, then shall be synonymous with . Otherwise shall be a synonym for . The type shall satisfy the requirements of NullablePointer.
template < class T , class R = retain_traits < T >> struct retain_ptr { using element_type = T ; using traits_type = R ; using pointer = /* see below */ retain_ptr ( pointer , retain_object_t ); retain_ptr ( pointer , adopt_object_t ) noexcept ; explicit retain_ptr ( pointer ); retain_ptr ( nullptr_t ) noexcept ; retain_ptr ( retain_ptr const & ) noexcept ; retain_ptr ( retain_ptr && ) noexcept ; retain_ptr () noexcept ; ~ retain_ptr (); retain_ptr & operator = ( retain_ptr const & ); retain_ptr & operator = ( retain_ptr && ) noexcept ; retain_ptr & operator = ( nullptr_t ) noexcept ; void swap ( retain_ptr & ) noexcept ; explicit operator pointer () const noexcept ; explicit operator bool () const noexcept ; element_type & operator * () const noexcept ; pointer operator -> () const noexcept ; pointer get () const noexcept ; long use_count () const ; [[ nodiscard ]] pointer release () noexcept ; void reset ( pointer , retain_object_t ); void reset ( pointer , adopt_object_t ); void reset ( pointer p = pointer { }); };
7.4.1. retain_ptr constructors
retain_ptr ( pointer p , retain_object_t );
Effects: Constructs a that retains , initializing the stored
pointer with , and increments the reference count of if by way of .
Postconditions:
Remarks: If an exception is thrown during increment, this constructor will have no effect.
retain_ptr ( pointer p , adopt_object_t ) noexcept ;
Effects: Constructs a that adopts , initializing the stored
pointer with .
Postconditions:
Remarks: 's refrence count remains untouched.
explicit retain_ptr ( pointer p );
Effects: Constructs a by delegating to another constructor via . If does not exist, is constructed as if by .
Postconditions:
Remarks: If an exception is thrown, this constructor will have no effect.
retain_ptr () noexcept ;
Effects: Constructs a object that retains nothing,
value-initializing the stored pointer.
Postconditions:
retain_ptr ( retain_ptr const & r );
Effects: Constructs a by extending management from to . The stored pointer’s reference count is incremented.
Postconditions:
Remarks: If an exception is thrown, this constructor will have no effect.
retain_ptr ( retain_ptr && r ) noexcept ;
Effects: Constructs a by transferring management from to . The stored pointer’s reference count remains untouched.
Postconditions: yields the value yielded before the
construction.
7.4.2. retain_ptr destructor
~ retain_ptr ();
Effects: If , there are no effects. Otherwise, .
7.4.3. retain_ptr assignment
retain_ptr & operator = ( retain_ptr const & r );
Effects: Extends ownership from to as if by calling . Returns:
retain_ptr & operator = ( retain_ptr && r ) noexcept ;
Effects: Transfers ownership from to as if by calling Returns:
retain_ptr & operator = ( nullptr_t ) noexcept ;
Effects: Postconditions: Returns:
7.4.4. retain_ptr observers
element_type & operator * () const noexcept ;
Requires: Returns:
pointer operator -> () const noexcept ;
Requires: Returns: Note: Use typically requires that be a complete type.
pointer get () const noexcept ;
Returns: The stored pointer
explicit operator pointer () const noexcept ;
Returns:
explicit operator bool () const noexcept ;
Returns:
long use_count () const ;
Returns: Value representing the current reference count of the current stored
pointer. If is not a valid expression, is
returned. If , is returned.
Remarks: Unless otherwise specified, the value returned should be considered stale.
7.4.5. retain_ptr modifiers
[[ nodiscard ]] pointer release () noexcept ;
Postconditions: Returns: The value had at the start of the call to .
void reset ( pointer p , retain_object_t );
Effects: Assigns to the stored pointer, and then if the old value of
stored pointer , was not equal to , calls . Then if is not equal to , is called. Postconditions:
void reset ( pointer p , adopt_object_t );
Effects: Assigns to the stored pointer, and then if the old value of
stored pointer , was not equal to , calls . Postconditions:
void reset ( pointer p = pointer { })
Effects: Delegates assignment of to the stored pointer via . Postconditions:
void swap ( retain_ptr & r ) noexcept ;
Effects: Invokes on the stored pointers of and .
7.4.6. retain_ptr specialized algorithms
template < class T , class R > void swap ( retain_ptr < T , R >& x , retain_ptr < T , R >& y ) noexcept ;
Effects: Calls
template < class T , class R > auto operator <=> ( retain_ptr < T , R > const & , retain_ptr < T , R > const & ) noexcept = default ;
Returns:
template < class T , class R > auto operator <=> ( retain_ptr < T , R > const & , nullptr_t ) noexcept = default ;
Returns:
template < class T , class R > strong_ordering operator <=> ( nullptr_t , retain_ptr < T , R > const & y ) noexcept ;
Returns: