Chapter 2. Reference-counted objects

Index

Comparison with other implementations of reference-counted objects
Objects
Pointers and references
x::const_ref and x::const_ptr
create() - create reference-counted objects
Range-based iteration on reference-counted objects
Specifying a custom base
Multiple inheritance and casting
Restricting overload resolution
Creating an x::ref or an x::ptr from this
Second phase constructor
Using deduction guides with an x::ref or an x::ptr
Multiple inheritance with reference-counted objects
Private inheritance of reference-counted objects
Comparing x::refs and x::ptrs
isa()
Weak pointers
Reference and pointer traits
Constructing a collection of references
Destructor callbacks and mcguffins
A basic destructor callback
A destructor callback guard
Revocable destructor callbacks
Invoking a destructor callback when any of several objects gets destroyed
Waiting for a cascading destructor
Weak containers
Mcguffin containers
Circular strong references

Nearly all objects in LibCXX are reference-counted objects, similar to what std::shared_ptr implements, but with notable differences as described here. In this context, this is not the same kind of a reference that's a part of the C++ language itself. For clarity, native references will refer to traditional C++ references, while the general usage of the term reference pointer, or a reference, refers to the construct described here.

Reference-counted objects do not get accessed with an ordinary pointer, but by using a reference pointer, an x::ref or an x::ptr. A reference-counted object gets instantiated in dynamic scope using a variadic create() function that forwards its arguments to the object's constructor; create() instantiates the object in dynamic scope and creates the first, initial reference pointer to the newly-instantiated object.

Reference pointers look like ordinary pointers. They have * and -> operators with the expected behavior. Reference pointers may be freely copied and passed around.

create() replaces new. new does not get used to construct reference- counter object in dynamic scope, and must not be used. Similarly, delete does not get used, explicitly, with reference-counted objects. When the last reference to an object goes out of scope and longer exists, the last reference pointer takes care of destroying the object with a delete, and invoking any destructor callbacks.

Warning

pthread_cancel(3) cannot be used in code that uses LibCXX. pthread_cancel(3) terminates a thread without unwinding the stack. The stack may contain references to reference-counted objects, or other objects whose destructors contain critical functionality.

Using pthread_cancel(3) will result in memory leaks, deadlocks, and other unstable behavior. This is likely to be the case with any C++-based process of non-trivial complexity, not just LibCXX. The only safe way to forcibly terminate the thread is by throwing an exception that unwinds the entire stack frame. x::msgdispatcherObj provides a convenient message-based thread design pattern which supplies a stop() method that sends a message to the running thread that causes it to throw an exception and terminate, in an orderly manner.

Comparison with other implementations of reference-counted objects

This implementation of reference-counted objects is similar to other similar implementations, notably shared_ptr in the C++ library, however there are fundamental differences. The key differences are:

  • A shared_ptr contains a pointer to a small heap-allocated object that tracks the reference count, and a second pointer to the actual object:

  • LibCXX's reference-counted objects use a different approach that's similar to Java's implementation of managed objects. LibCXX's reference-counted objects are derived from the x::obj superclass, which tracks the reference count. There are two main reference pointer classes, x::ref and x::ptr, holding a single pointer to the object:

  • shared_ptr's * and -> operators dereference a mutable object, and dereferencing a nullptr is undefined behavior. x::ptr can also be a nullptr, but dereferencing a nullptr is not undefined behavior: an exception gets thrown, with defined semantics. x::ref's * and -> operators do not check for a nullptr because x::ref cannot be a nullptr. It always refers to an object. This is enforced by contract. The nullptr gets checked when an x::ref gets assigned to. x::ref is proven, by contract, never to contain a nullptr; hence its * and -> directly dereferences the internal pointer to the underlying object.

    An x::ref lvalue is convertible to an x::ptr lvalue without a temporary. Converting an x::ptr to an x::ref checks for a nullptr at the time the conversion takes place, where an exception gets thrown. An x::ptr may be passed to a function that takes an x::ref argument. If the caller supplies a nullptr, the exception gets thrown in the caller's context, for violating the contract. The backtrace accurately points to the guilty party, not the function, but its caller.

  • x::ptr and x::ref are analogous to natural C++ pointers and references. Similarly, LibCXX defines an x::const_ptr and an x::const_ref, that dereference to constant objects. This is directly analogous to a std::iterator and a std::const_iterator. Nothing similar is available with a shared_ptr. A shared_ptr<T> is convertible to a shared_ptr<const T>, but doing that for a function call requires the construction of a temporary. An x::ptr lvalue converts to an x::const_ptr lvalue without a temporary, ditto for x::ref and an x::const_ref.

The primary benefit of using shared_ptr is that it allows any class to acquire reference-counting semantics without modification. Standard C++ library classes, such as various stream objects, can be easily wrapped into a reference-counted framework.

However, there are several disadvantages to shared_ptr that x::ref and x::ptr aim to address.

  • Each reference-counted object requires allocating another small object, the reference counter, from the heap. Over the long term, this is more likely to increase heap fragmentation, and heap usage, especially in long running applications, as reference-counted objects get created and destroyed. make_shared partially mitigates this, however the end result has to be compatible with the stock shared_ptr and that introduces additional behind the scenes complexity.

  • There is no straightforward way to prove, by contract, that a shared_ptr instance is not a nullptr.

  • It's not possible to construct a shared_ptr from this, unless the class explicitly inherits from an enable_shared_from_this superclass. But this cancels out shared_ptr primary benefit of wrapping any arbitrary class. Furthermore, this approach becomes somewhat difficult when multiple inheritance gets involved.

LibCXX stores the reference count in each object's superclass, x::obj which is directly accessible by any subclass. A new x::ref or an x::ptr gets trivially constructed from this, without much fanfare. This creates a new reference, and increments the object's reference count.

Furthermore, since the counter is a part of the object, it does not need to be allocated separately, on the heap. The only drawback is that, unlike with shared_ptr, it's not possible to implement reference-counted semantics for an arbitrary class. It is necessary to declare a subclass that multiply inherits from that class, and from x::obj.

x::refs and x::ptrs work with multiple inheritance without any special effort. Each reference-counted class virtually inherits from x::obj, which automatically does the right thing when multiple reference-counted classes get inherited from.