C++ / a working model

39 / 163   ·   C++11   ·   11 min

shared_ptr: Control Blocks and Thread-Safety Boundaries

Keep this sentence

What shared_ptr shares is a set of destruction responsibilities, usually recorded in a control block with owner counts, a deleter, and related information. The control block lets different shared_ptr instances concurrently manage the same object, but it does not protect the object’s internal data, and it does not allow unsynchronized modification of the same shared_ptr variable.

In this lesson
  1. The Same Address Is Not the Same Ownership
  2. The Stored Pointer and the Managed Object Can Differ
  3. Thread Safety Has Three Object Layers
  4. Example
  5. Exercise

The Same Address Is Not the Same Ownership

Copying a shared_ptr joins an existing ownership group; when the last strong owner releases, the agreed deleter is called. Typical implementations store strong and weak counts, a deleter, and allocation information in a control block, but that field layout and any fixed size of shared_ptr are not interface guarantees. Do not infer Standard requirements from two addresses in a debugger.

Constructing two shared_ptr objects separately from the same raw address creates two unaware ownership groups and usually double-deletes. You must copy the original shared_ptr, or use a conversion that keeps the same ownership group. The address from get is only an observation; it cannot be used to manufacture a separate destruction responsibility.

The Stored Pointer and the Managed Object Can Differ

Aliasing construction can make a shared_ptr point at a member while keeping the whole host object alive. In the example the field pointer points at Bundle::value, but what is owned is Bundle’s shared lifetime. After the outer owner is reset, the field pointer still keeps the host alive until it too is reset.

make_shared commonly allocates the object and the control block together, reducing allocation count. When the last strong reference disappears the object is still destroyed by the rules, but if a weak_ptr remains the control block must stay; the combined storage may therefore be returned later. Recognize this cost when a large object meets a long-lived weak observer.

Thread Safety Has Three Object Layers

Different shared_ptr instances that share the same control block may be copied or reset on different threads without extra protection of the reference count. If the same shared_ptr variable is modified by one thread and accessed by another at the same time, you need a lock, or C++20’s atomic<shared_ptr<T>>.

The third layer is T itself: two legitimate shared_ptr objects writing an ordinary integer at the same time can still data-race. The control block only maintains lifetime; it does not lock T. use_count in a concurrent setting is only a snapshot observation and cannot assert exclusive modification rights; the example’s count assertions are only for a deterministic scene with no other threads.

Pitfalls

  • const shared_ptr<T> only restricts the handle and usually still allows modifying T; use shared_ptr<const T> when you need a read-only access type.
  • use_count equal to one is not a synchronization protocol; another thread may obtain a new owner by locking a weak pointer.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <memory>

struct Bundle {
    static int alive;
    int value;
    Bundle() : value(42) { ++alive; }
    ~Bundle() { --alive; }
};
int Bundle::alive = 0;

int main() {
    auto owner = std::make_shared<Bundle>();
    std::shared_ptr<int> field(owner, &owner->value);
    assert(owner.use_count() == 2);
    owner.reset();
    assert(Bundle::alive == 1);
    assert(*field == 42);
    field.reset();
    assert(Bundle::alive == 0);
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread memory-shared-ptr.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

Two threads each hold a copy of shared_ptr<Counter> and both execute ++p->value. Is reference-count safety enough?

Show a reference answer

No. Control-block operations on the two handles are guaranteed, but if value is an ordinary integer the two unsynchronized writes are a data race. Give Counter’s update operations the same mutex, or design the independent counter as a suitable atomic type. atomic<shared_ptr<Counter>> only solves publishing and replacing the handle; it does not automatically make Counter’s fields atomic.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog