Below is some sample code showing my use case. I have a PIMPL where the implementation can be shared (it's just a bunch of expensively-produced data) but the implementation can be destroyed when it's no longer needed. An instance of the class HasImpl
uses a shared pointer to Impl
, and the class definition includes a static weak_ptr
to Impl
that acts as a "pointer dispenser" to new instances of HasImpl
, giving them a handle to the Impl
if one exists already.
The sample has two alternatives for calling weak_ptr::lock
--one that assumes that the the answer to questions 1-3 below are all "yes", and another that does not. The only reason I'd prefer that weak_ptr::lock
is thread-safe is that there could be multiple threads trying to get a copy of the pointer to Impl
, and if lock
is thread-safe, most threads of execution won't have to pass a static variable definition (where the thread would have to check to see if it were already initialized) and won't have to compete to acquire a mutex.
/* In HasImpl.h */
class HasImpl {
public:
HasImpl();
private:
class Impl;
static std::weak_ptr<Impl> sharedImplDispenser;
std::shared_ptr<Impl> myPtrToSharedImpl;
}
/* In HasImpl.cpp */
class HasImpl::Impl {
public:
Impl(); //constructor that takes a lot of time to run
//Lots of stuff, expensively produced, accessable to HasImpl through a shared_ptr to Impl
}
/* hypothetical constructor if weak_ptr::lock is thread-safe */
HasImpl::HasImpl() : myPtrToSharedImpl{sharedImplDispenser.lock()}
{
if (!myPtrToSharedImpl) {
static std::mutex mtx;
std::lockguard<std::mutex> lck(mtx);
myPtrToSharedImpl = sharedImplDispenser.lock();
if (!myPtrToSharedImpl) {
const auto new_impl{std::make_shared<Impl()};
sharedImplDispenser = new_impl; // the only place in the program where a value is assigned to sharedImplDispenser
myPtrToSharedImpl = new_impl;
}
}
}
/* hypothetical constructor if weak_ptr::lock is not thread-safe */
HasImpl::HasImpl()
{
static std::mutex mtx;
std::lockguard<std::mutex> lck(mtx);
myPtrToSharedImpl = sharedImpl.lock();
if (!myPtrToSharedImpl) {
const auto new_impl{std::make_shared<Impl()};
sharedImplDispenser = new_impl; // the only place in the program where a value is assigned to sharedImplDispenser
myPtrToSharedImpl = new_impl;
}
}
std::weak_ptr
is not empty and was assigned a pointer sometime in the distant past, will the control block be ok if one thread calls weak_ptr::lock
while another thread may be calling weak_ptr::lock
?weak_ptr::lock
while another thread may be assigning a ptr to an empty weak_ptr safe enough? That is, will the value either return nullptr or the new pointer? I don't care if the nullptr is spurious (that is, that assignment has occurred but the other threads don't know about it yet). I just don't want to corrupt the control block or obtain an invalid pointer value from the call.weak_ptr::lock
while the last shared_ptr to the object is being destroyed thread safe?std::atomic<std::weak_ptr<T>>
in C++20 fix the issue? The standard explicitly says that weak_ptr::lock
is "executed atomically". So that answers 1 and 3.
For #2, if you're asking about assigning to the same weak_ptr
, then it's a data race. Operations that change a shared state's use_count
don't provoke a data race, but copying or manipulating the weak_ptr
itself is doing more than just poking at use_count
.
But if you're talking about locking one weak_ptr
while nulling out a different weak_ptr
that are both talking to the same shared state, that's fine. The two only interact through the shared state's count, which is stated to be fine.
And yes, atomic<weak_ptr<T>>
would allow you to manipulate the same object from multiple threads.