Atomic Reference Counting
You probably overestimate the threadsafety a shared_ptr provides.
The essence of atomic ref counting is to ensure that if two different instances of a shared_ptr
(that are managing the same object) are accessed/modified, there will be no race condition. However, shared_ptr
doesn't ensure thread safety, if two threads access the same shared_ptr
object (and one of them is a write). One example would be e.g. if one thread dereferences the pointer, while the other resets it.
So about the only thing shared_ptr
gurantees is that there will be no double delete and no leak as long as there is no race on a single instance of a shared_ptr (It also doesn't make accesses to the object it points to threadsafe)
As a result, also creating a copy of a shared_ptr is only safe, if there is no other thread that could delete/reset it at the same time (you could also say, it is not internally synchronized). This is the scenario you describe.
To repeat it once more: Accessing a single shared_ptr
instance from multiple threads where one of those accesses is a write to the pointer is still a race condition.
If you want to e.g. copy a std::shared_ptr
in a threadsafe manner, you have to ensure that all loads and stores happen via std::atomic_...
operations which are specialized for shared_ptr
.
Your scenario is not possible because Thread B should have been created with an incremented refcount already. Thread B should not be incrementing the ref count as the first thing it does.
Let's say Thread A spawns Thread B. Thread A has the responsibility to increment the ref count of the object BEFORE creating the thread, to guarantee thread safety. Thread B then only has to call release when it exits.
If Thread A creates Thread B without incrementing the ref count, bad things might happen as you've described.
The implementation doesn't provide or require such a guarantee, avoidance of the behavior you are describing is predicated on the proper management of counted-references, usually done through a RAII class such as std::shared_ptr
. The key is to entirely avoid passing by raw pointer across scopes. Any function which stores or retains a pointer to the object must take a shared pointer so that it can properly increment the ref count.
void f(shared_ptr p) { x(p); // pass as a shared ptr y(p.get()); // pass raw pointer}
This function was passed a shared_ptr
so the refcount was already 1+. Our local instance, p
, should have bumped the ref_count during copy-assignment. When we called x
if we passed by value we created another ref. If we passed by const ref, we retained our current ref count. If we passed by non-const ref then it's feasible that x()
released the reference and y
is going to be called with null.
If x()
stores/retains the raw pointer, then we may have a problem. When our function returns the refcount might reach 0 and the object might be destroyed. This is our fault for not correctly maintaining ref count.
Consider:
template<typename T>void test(){ shared_ptr<T> p; { shared_ptr<T> q(new T); // rc:1 p = q; // rc:2 } // ~q -> rc:1 use(p.get()); // valid} // ~p -> rc:0 -> delete
vs
template<typename T>void test(){ T* p; { shared_ptr<T> q(new T); // rc:1 p = q; // rc:1 } // ~q -> rc:0 -> delete use(p); // bad: accessing deleted object}