Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is std::weak_ptr::expired optimized away?

In the following code, while ( !Ref.expired() ); is joyfully optimized into an infinite loop. If the line of code is changed to while ( !Ref.lock() );. everything works as expected. So two questions really:

1) How can the compiler optimize away expired when std::weak_ptr::expired() accesses a memory-fenced counter?

2) Is Ref.lock() actually safe, or could this too be optimized away?

Sample code below.

#include <iostream> #include <memory> #include <thread> #include <chrono>  class A { public:      A()      {          m_SomePtr = std::make_shared<bool>( false );     }      virtual ~A()     {         std::weak_ptr<bool> Ref = m_SomePtr;         m_SomePtr.reset();          // Spin (will be optimised into an infinite loop in release builds)         while ( !Ref.expired() );     }      std::shared_ptr<bool> GetPtr() const { return m_SomePtr; }  private:     std::shared_ptr<bool> m_SomePtr; };  class B { public:     B( std::shared_ptr<bool> SomePtr ) : m_Ref( SomePtr ) {}      void LockPtr() { m_SomePtr = m_Ref.lock(); }     void UnLockPtr() { m_SomePtr.reset(); }  private:     std::shared_ptr<bool> m_SomePtr;     std::weak_ptr<bool> m_Ref; };  int main() {     std::unique_ptr<A> a( new A() );     std::unique_ptr<B> b( new B( a->GetPtr() ) );      b->LockPtr();      std::cout << "Starting " << std::endl;      std::thread first( [&]()     {         std::this_thread::sleep_for( std::chrono::seconds( 5 ) );         b->UnLockPtr();     } );      std::thread second( [&]()     {         a.reset( nullptr );     } );      first.join();     second.join();      std::cout << "Complete" << std::endl;     return 0; } 
like image 354
Puff Of Hot Air Avatar asked Jan 23 '15 14:01

Puff Of Hot Air


People also ask

Is std :: Weak_ptr thread safe?

Note that the control block used by std::weak_ptr and std::shared_ptr is thread-safe: different non-atomic std::weak_ptr objects can be accessed using mutable operations, such as operator= or reset , simultaneously by multiple threads, even when these instances are copies or otherwise share the same control block ...

How is Weak_ptr implemented?

To implement weak_ptr , the "counter" object stores two different counters: The "use count" is the number of shared_ptr instances pointing to the object. The "weak count" is the number of weak_ptr instances pointing to the object, plus one if the "use count" is still > 0.


1 Answers

Your program is incorrect; the shared-ownership pointer facilities are not intended to be used for synchronization.

[intro.multithread]/24:

The implementation may assume that any thread will eventually do one of the following:
— terminate,
— make a call to a library I/O function,
— access or modify a volatile object, or
— perform a synchronization operation or an atomic operation.

std::weak_ptr::expired() is not a synchronization operation or an atomic operation; all the Standard says is that it does not introduce a data race. Since the resolution to Library defect 2316, std::weak_ptr::lock() is considered an atomic operation, so to answer 2) your code using Ref.lock() is valid as of C++14.

Now, it's true that if you were to attempt to create your own library implementation of weak_ptr using the language and library facilities, it would necessarily use the synchronization and/or atomic operation facilities, so a user-provided weak_ptr::expired() would be OK to spin on (the implementation would be obliged to ensure that the thread eventually made progress, per [intro.multithread]/2 and /25). But an implementation is not obliged to restrict its own library to the language and library facilities.

I'm not entirely sure how the compiler is optimizing away the access to expired(). I'd guess that the MSVC library is exploiting aspects of the x86 memory model that the compiler/optimizer observes are not guaranteed by the C++ memory model.

like image 149
ecatmur Avatar answered Sep 19 '22 11:09

ecatmur