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
Creating an x::ref or an x::ptr from this
Multiple inheritance with reference-counted objects
Private inheritance of reference-counted objects
Weak pointers
Destructor callbacks and mcguffins
Using a functor as a destructor callback
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
Circular strong references

Nearly all objects in LIBCXX are reference-counted objects, which are 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 defined in the C++ language. 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 on the heap, using a variadic create() function that forwards its arguments to the object's constructor; create() instantiates the object on the heap and constructs the first, initial reference pointer to the newly-instantiated object.

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

new does not get used, explicitly, to instantiate objects on the heap, this is taken care of by create(). 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:

  • gcc's implementation of shared_ptr is derived from an implementation in the Boost library, which appears to be not thread-safe. The proposal that's discussed in that email thread to address this defect will have a major thread performance impact.

  • A shared_ptr contains a pointer to a small heap-allocated object that stores 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 an x::obj superclass, which stores 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 ill-formed. x::ptr can also be a nullptr, but dereferencing a nullptr is not ill-formed: 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 must always refer to an object. This is enforced. The nullptr gets checked upon an assignment of a x::ref. x::ref is proven, by contract, never to contain a nullptr; hence its * and -> directly dereference 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 advantage of the 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 seek 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.

  • 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 constructed directly 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.