简体   繁体   中英

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;
}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM