https://gcc.gnu.org/bugzilla/show_bug.cgi?id=118854
--- Comment #10 from Jonathan Wakely <redi at gcc dot gnu.org> ---
(In reply to Nicholas Williams from comment #8)
> Notably, the copy constructor is declared noexcept. While I know that's
> syntatically about *exceptions*, the *implication* is that it should be safe
> to call that construct and not experience errors. A segfault/heap-after-use
> is an error.
No, that's not what noexcept means AT ALL.
It doesn't imply that the object can't be misused and result in undefined
behaviour, which is what happens here.
> More importantly, though, the C++ documentation for shared_ptr mentions
> thread safety in a few ways:
>
> > To satisfy thread safety requirements, the reference counters are typically
> > incremented using an equivalent of std::atomic::fetch_add with
> > std::memory_order_relaxed.
>
> And:
>
> > If multiple threads of execution access the same shared_ptr object without
> > synchronization and any of those accesses uses a non-const member function
> > of shared_ptr then a data race will occur
>
> But the copy constructor argument is const, which means only const member
> functions / data members of that argument can be used, so it should be
> thread safe.
There's a data race with the shared_ptr destructor, which is not required to be
thread-safe. You cannot make a copy of an object while it's being destroyed.
> There is a strong implication throughout the totality of the
> documentation/spec for shared_ptr and its copy constructor that copying a
> shared_ptr in a multi-threaded environment should be safe.
Only if no other thread is modifying the shared_ptr while you're copying it.
Destroying it counts as modifying it (so does assigning to it, or calling
reset()).
Consider:
#include <memory>
#include <thread>
int main()
{
unsigned counter = 0;
std::thread t;
{
std::shared_ptr<int> p = std::make_shared<int>(1);
t = std::thread([&] { auto p2 = p; counter += *p2; });
} // p is destroyed here
t.join();
}
The thread makes a copy of the shared_ptr and then dereferences the copy, but
it does that while the shared_ptr p is being destroyed. That's a data race, and
undefined behaviour.
If you need to make copies of a shared_ptr while also modifying it, you need to
use std::atomic<std::shared_ptr<T>> (or std::experimental::atomic_shared_ptr).