Inside STL: The different types of shared pointer control blocks

Raymond Chen

We saw earlier that C++ standard library shared pointers use a control block to manage the object lifetime.

struct control_block
{
    virtual void Dispose() = 0;
    virtual void Delete() = 0;
    std::atomic<unsigned long> shareds;
    std::atomic<unsigned long> refs;
};

The control block has pure virtual methods, so it is up to derived classes to establish how to dispose and delete the control block.

If you ask a shared_ptr to take responsibility for an already-constructed pointer, then you get this:

template<typename T>
struct separate_control_block : control_block
{
    virtual void Destroy() noexcept override
    {
        delete ptr;
    }
    virtual void Delete() noexcept override
    {
        delete this;
    }
    T* ptr;
};

Added to the basic control block is a pointer to the managed object, which is deleted when the last strong reference goes away.

If you use make_shared or allocate_shared, then the control block and the managed object are placed in the same allocation. In that case, the control block looks like this:

template<typename T>
struct combined_control_block : control_block
{
    virtual void Destroy() noexcept override
    {
        ptr()->~T();
    }
    virtual void Delete() noexcept override
    {
        delete this;
    }
    T* ptr() { return reinterpret_cast<T*>(buffer); }

    // This buffer holds a "T"
    [[alignas(T)]] char buffer[sizeof(T)];
};

Added to the basic control block is a buffer suitable for holding a T object. When the shared_ptr is created, a T is placement-constructed in that buffer, and when the last strong reference goes away (Destroy()), it is destructed. Stephan T. Lavavej calls this the “We know where you live” optimization because the control block doesn’t need to store an explicit pointer to the buffer; it can derive it on the fly.

The reality is a little more complicated due to the need to store a deleter and possibly an allocator, but those are typically zero-length objects, so they get stored in a compressed pair with the other members.

In practice, when debugging, you don’t need to look past the reference counts in the control_block. The thing you really care about in the shared_ptr is the pointed-to object. If you ever look at the control block, it’s just to check whether there are any active strong references.

Bonus chatter: For C++20 make_shared<T[]>, there’s another version of the control block that also has a count member which specifies how many objects are in the storage.

7 comments

Discussion is closed. Login to edit/delete existing comments.

  • 紅樓鍮 0

    Actually, the functionality of separate_control_block<T> is subsumed by combined_control_block<std::unique_ptr<T>>, so technically you don’t need the former at all.

    • Aidan Garvey 0

      `seperate_control_block` contains a pointer to an object, but `combined_control_block` contains storage for the entire object, which means the control block’s size depends on the size of the object.
      So the former is still needed for constructing a shared pointer which takes ownership of a pre-existing object.

      • 紅樓鍮 1

        The non-combined allocation version of std::shared_ptr<T> can use combined_control_block<std::unique_ptr<T>> like this:

        shared_ptr(T *ptr)
            : object(ptr),
              control(new combined_control_block<std::unique_ptr<T>>({ptr}))
        {}

        The combined_control_block takes care to destroy the unique_ptr, which in turn takes care to delete ptr, so everything adds up.

        • Aidan Garvey 0

          Ah, looks like I didn’t read your original comment closely enough. I missed that you had a unique_ptr in the combined_control_block as opposed to just a `T`. I wonder why the STL uses two different versions.

  • Samuel Q 0

    What benefit is there to using `char buffer[sizeof(T)]` as opposed to directly having the field be `T obj`? (Unfortunately my knowledge of the gritty details of how object lifetimes work is rusty)

    • Kalle Niemitalo 0

      In Delete(), the `delete this;` statement calls the ~combined_control_block() destructor. If there were a `T obj;` data member, then ~combined_control_block() would call the destructor of that data member. But the T instance was already destructed by Destroy() and the second call could corrupt something, e.g. double free.

    • 紅樓鍮 0

      Also be careful to align buffer‘s memory address suitably with alignas(T). (And here Raymond seems to have made a typo: there should be no [[...]] surrounding the alignas(T) because alignas is a keyword and not an attribute.)

Feedback usabilla icon