Phantom and indulgent shared pointers

Raymond Chen

Last time, we looked at various ways to convert among different shared_ptrs. We finished with this diagram:

  Null control block Non-null control block
Null stored pointer Empty Phantom
Non-null stored pointer Indulgent Full

You are familiar with an empty shared pointer, which manages no object and has no stored pointer. You are also familiar with a full shared pointer, which manages an object and has a non-null stored pointer (to the managed object, or something whose lifetime is controlled by the managed object). But what about those other two guys?

In the upper right corner, you have the case of a shared pointer that manages an object but whose stored pointer is null, which I’ve called a phantom shared pointer. If you convert the shared pointer to a bool, it produces false, because you can’t use it to access anything. The phantom shared pointer looks empty at a casual glance, but it secretly manages an object behind the scenes. That secretly-managed object remains alive for no visible reason. There is no way to access that secretly-managed object, but it’s still there. It’s a phantom which follows you around.

struct Sample
    int value;
std::shared_ptr<Sample> p = std::make_shared<Sample>();
std::shared_ptr<char> q = std::shared_ptr<char>(p, nullptr);
p = nullptr;

In the above example, we start by creating a shared pointer to a freshly-constructed Sample object. We then use this shared pointer to provide the control block to a new aliasing shared pointer whose stored pointer is nullptr. Finally, we clear the original shared pointer, so that the only reference to the Sample is in the phantom shared pointer q. The Sample is now being kept alive by what looks like a null pointer!

In the opposite corner, you have a shared pointer with no control block, but with a non-null stored pointer, which I’ve called an indulgent shared pointer because it points to something, yet owns nothing. “Go ahead, have fun with this pointer all you like, I don’t care!”

int globalVariable;

std::shared_ptr<int> p(std::shared_ptr<int>(), &globalVariable);

We use the aliasing constructor for shared_ptr<int> which takes another shared_ptr<int> (which provides the control block) and a raw pointer (which serves as the stored pointer). The donor shared_ptr is empty and has no control block. We give that nonexistent control block to the aliasing constructor, and the result is a shared pointer that owns nothing, yet which points to something, namely, globalVariable.

This style of shared pointer is useful if you need a shared pointer that points to memory with static storage duration. The pointer is valid for the duration of the process, so you don’t need a control block to manage the object’s lifetime: The object’s lifetime is “forever”.

How can you detect whether you have one of these unusual shared pointers?

If you treat a shared_ptr as a bool, the resulting value tells you whether the stored pointer is non-null. On the other hand, if you ask use_count(), it will return a nonzero value if there is a managed object. Now we can finish our table:

  Null control block
(use_count() == 0)
Non-null control block
(use_count() != 0)
Null stored pointer
Empty Phantom
Non-null stored pointer
Indulgent Full

Note that phantom shared pointers are falsy, and indulgent shared pointers are truthy. I couldn’t think of a synonym for indulgent that begins with a “t”, sorry.

In code, we can detect the four cases by using use_count() and get() (or equivalently, converting to bool).

void detect(std::shared_ptr<T> const& p)
    bool has_control_block = p.use_count();
    bool has_pointer = p.get();
    // equivalently, has_pointer = static_cast<bool>(p);

    if (has_control_block && has_pointer) {
        // full
    } else if (has_control_block && !has_pointer) {
        // phantom
    } else if (!has_control_block && has_pointer) {
        // indulgent
    } else { // !has_control_block && !has_pointer)
        // empty

1 comment

Comments are closed. Login to edit/delete your existing comments

  • 紅樓鍮 0

    It’s unfortunate that use_count() costs an atomic load, which probably inhibits optimizations such as constant propagation as well. In fact, C++ Reference notes on its page on unique_ptr::use_count that

    Common use cases include

    • comparison with 0. …
    • comparison with 1. …

    Furthermore, comparison with 1 is generally useless since if you find yourself tempted to do this, what you actually want to do is probably somehow “turn the shared_ptr into a unique_ptr” in a thread-safe manner, which, in the presence of possible concurrent weak_ptr promotions, cannot be accomplished using the current interface of shared_ptr. I think the 2 common use cases of use_count() should therefore be better served by

    // A deleter that points to a control block used by the `shared_ptr` facility.
    // When called, It ignores the argument pointer
    // and makes the control block delete its managed object.
    class shared_ptr_deleter;
    template <typename T>
    class shared_ptr {
      // Returns `true` if the `shared_ptr` manages an object.
      constexpr bool is_owning() const noexcept;
      // Acquire the control block on condition that there are no other
      // `unique_ptr`s currently managing the same object.
      // If the condition is met, the reference count is decremented to 0
      // but the object is not deleted, and the stored pointer and
      // the control block pointer are used to construct the returned
      // `unique_ptr<T, shared_ptr_deleter>`.
      // If the condition is not met, both the returned `unique_ptr`
      // and its deleter are null.
      // The behavior of calling this member function on a
      // non-owning `shared_ptr` is unspecified.
      unique_ptr<T, shared_ptr_deleter>
      try_into_unique_ptr() && noexcept;
      // Similar to `try_into_unique_ptr`, but blocks until its condition is met.
      unique_ptr<T, shared_ptr_deleter>
      into_unique_ptr(/* timeout parameters... */) && noexcept;
      /* ... */

Feedback usabilla icon