C++ Language Concepts

[c++] smart pointer - shared_ptr(life cycle and thread)

doyyy_0 2025. 5. 22. 10:13

Smart pointers manage objects and automatically perform dynamic deallocation when their lifetime ends.
std::shared_ptr internally holds two pointers:

  1. Stored pointer – the actual pointer to the object, which can be accessed using .get().
  2. Control block pointer – a pointer to metadata that manages reference counts and other control information.

To understand this more precisely, here's a structure of what a control block typically contains:

struct ControlBlock {
    std::atomic<int> shared_count;
    std::atomic<int> weak_count;
    void* managed_ptr;          // →  MyStruct*
    Deleter deleter;            // → used to call delete
	Allocator allocator;        // → used to deallocate memory
};

 


Why does shared_ptr store both a stored pointer (i.e., the one returned by .get()) and a separate managed object pointer (e.g., void* managed_ptr in the control block)?

 

Let’s take a look at the following example:



#include <iostream>
#include <memory>

struct MyStruct {
    int x;
    int y;
};

int main() {
    // After Mystruct Object, owning as shared_ptr
    std::shared_ptr<MyStruct> obj = std::make_shared<MyStruct>();
    obj->x = 42;
    obj->y = 99;

    // Making shared_ptr pointing member x of obj
    // using aliasing constructor!
    std::shared_ptr<int> aliasX(obj, &obj->x);

    std::cout << "aliasX: " << *aliasX << std::endl; // 42
    std::cout << "use_count: " << obj.use_count() << std::endl; // 2

    obj.reset(); // original obj reseted

    std::cout << "aliasX after obj.reset(): " << *aliasX << std::endl; // 42 
    std::cout << "use_count: " << aliasX.use_count() << std::endl; // 1

    // if asliasx is deleted, Mystruct is deleted
}



The control block of obj and aliasX is the same — the only difference is the stored pointer, i.e., the one returned by .get().

The key point in this code is that aliasX was created using the aliasing constructor, so it points to a specific member (&obj->x) rather than the full object.

Although aliasX stores an int* (i.e., &obj->x) as its internal pointer, it shares the same control block as obj.
Therefore, even if obj.reset() is called and obj releases its ownership, the MyStruct instance remains alive as long as aliasX still exists.

This separation between the stored pointer and the managed object pointer allows shared_ptr to refer to a sub-object while still maintaining ownership of the entire object.

 

To reinforce the idea that the stored pointer and the control block pointer are conceptually different, let's look at another example:

 

Foo* raw = new Foo();
std::shared_ptr<Foo> p2(raw); // ❌


A Foo object is first allocated using new,

and then a separate control block is allocated internally by shared_ptr.
In other words, two dynamic allocations occur: one for the object itself and one for the metadata (control block).

 

To avoid this overhead, it’s much safer and more efficient to use std::make_shared, which performs a single memory allocation for both the object and its control block.

Here's how you can do it:

auto p = std::make_shared<Foo>(); // ✔️  Object + Control block in once assigning



Copying a shared_ptr is thread-safe.


When a shared_ptr is copied, it simply increments an internal reference counter.
This increment operation is implemented using a mechanism similar to std::atomic::fetch_add,
which means it is safe to copy the same shared_ptr from multiple threads simultaneously.

std::shared_ptr<Foo> sp = std::make_shared<Foo>();

std::thread t1([=]() {
    std::shared_ptr<Foo> local = sp; // ✔️ copy -> safe
});

std::thread t2([=]() {
    std::shared_ptr<Foo> another = sp; // ✔️ copy -> safe
});



However, if multiple threads try to modify a shared_ptr (e.g., by calling reset() or reassigning it),
such access is not thread-safe.


In these cases, you should either mark the pointer as const to prevent modification,
or use std::atomic operations to synchronize access and avoid data races.

 

Let’s look at two examples

1. A dangerous version without synchronization

#include <iostream>
#include <memory>
#include <thread>

class Object {
public:
    Object()  { std::cout << "Object constructed\n"; }
    ~Object() { std::cout << "Object destructed\n"; }
};

std::shared_ptr<Object> shared;

void resetInThread(const std::string& name) {
    std::cout << name << " is resetting shared\n";
    shared.reset(); // ❗ data race can be occured
    std::cout << name << " finished resetting\n";
}

int main() {
    shared = std::make_shared<Object>();

    std::thread t1(resetInThread, "Thread 1");
    std::thread t2(resetInThread, "Thread 2");

    t1.join();
    t2.join();
}



2. A safe version using std::atomic_store for synchronization

#include <iostream>
#include <memory>
#include <thread>
#include <atomic>

class Object {
public:
    Object()  { std::cout << "Object constructed\n"; }
    ~Object() { std::cout << "Object destructed\n"; }
};

std::shared_ptr<Object> shared;

void resetInThread(const std::string& name) {
    std::cout << name << " is resetting shared\n";
    std::atomic_store(&shared, std::shared_ptr<Object>(nullptr)); // ✅ safe synchronization
    std::cout << name << " finished resetting\n";
}

int main() {
    std::atomic_store(&shared, std::make_shared<Object>());

    std::thread t1(resetInThread, "Thread 1");
    std::thread t2(resetInThread, "Thread 2");

    t1.join();
    t2.join();
}


 

reference 

https://en.cppreference.com/w/cpp/memory/shared_ptr

http://en.cppreference.com/w/cpp/memory/unique_ptr