1

The documentation for std::weak_ptr on cppreference says this

Effectively returns expired() ? shared_ptr<T>() : shared_ptr<T>(*this), executed atomically.

And my judgement and other SO answers have confirmed that the following is not prone to races

int main() {
    auto s_ptr = std::make_shared<T>(...);
    auto w_ptr = std::weak_ptr<T>{s_ptr};

    auto one = std::thread{[&]() {
        auto ptr = w_ptr.lock();
        if (ptr) { ... }
    }};
    s_ptr = std::make_shared<T>(...);

    one.join();
}

However can this reliably be used to shadow computation in a program? By shadowing I mean something like this

auto s_ptr = std::make_shared<T>(...);
auto w_ptr = std::weak_ptr<T>{s_ptr};

// thread one
while (...) {
    auto ptr = w_ptr.lock();
    cout << "Value contained is " << *ptr << endl;
}

// thread two
while (...) {
     // do the heavy computation on the side
     auto new_value = fetch_new_value();

     // then "atomically" swap
     s_ptr = std::make_shared<T>(std::move(new_value));
}

The confusing part here is what .lock() returns. Can it return a nullptr? All the documentation says is that the operation will be executed atomically. Doesn't say what this mutual exclusion means. Can there be a state in shared_ptr::operator= where the pointer is null? And can the weak_ptr access this state? The documentation for shared_ptr::operator= on cppreference does not seem to mention this.

Curious
  • 20,870
  • 8
  • 61
  • 146
  • 1
    I cannot cite chapter & verse, but I believe that shared_ptr and its little buddy weak_ptr are thread safe. The `auto ptr = w_ptr.lock()` can indeed return `nullptr` if all of the shared_ptr's have been destructed. In my opinion, shared_ptr and weak_ptr are two of the best features of C++11, even when not in a threaded scenario. – Eljay Dec 09 '17 at 20:29
  • 2
    And you might want to use this idiomatic pattern: `if (auto ptr = w_ptr.lock()) { cout << "Value contained is " << *ptr << endl; } else { cout << "Value is dead, Jim." << endl; }` – Eljay Dec 09 '17 at 20:30
  • Yeah, I just realized these two can be nice substitutes for the `atomic_...` stuff for `shared_ptr`. As long as the above is valid i.e. (.lock() doesn't return `nullptr` as long as the `shared_ptr` it was referring to was valid) – Curious Dec 09 '17 at 20:30
  • @Eljay I didn't include that on purpose. In the context of this question, if we assume the shared_ptr itself doesnt get destroyed, is that condition needed? I guess that itself is the question I wanted to ask – Curious Dec 09 '17 at 20:31
  • 1
    :-) Yes, if it is reliable that the share_ptr isn't destroyed, the weak_ptr will not return nullptr. (But that may become a "future bug" if someone refactors the code.) – Eljay Dec 09 '17 at 20:34
  • @Eljay if you could point me to a source (or put up a convincing argument that proves why the converse would be invalid) that confirms this in an answer I would accept it – Curious Dec 09 '17 at 20:35
  • 1
    Hmmm, here's a more detailed answer. • https://stackoverflow.com/questions/14482830/stdshared-ptr-thread-safety • The key point being that only the control block itself is thread-safe. – Eljay Dec 09 '17 at 20:40
  • 1
    From what I can see as soon as your shared pointer gets a new value, your weak pointer will start returning `nullptr` (because the original object got destroyed). But there will be no undefined behavior (because the handoff is atomic) – Galik Dec 09 '17 at 20:45
  • @Galik uh, I should have tried this before asking https://wandbox.org/permlink/UEYf1kn3ivfmMnwY. Anyway, I'll update the question – Curious Dec 09 '17 at 20:50
  • You need to get a new weak pointer every time your shared pointer gets reset. – Galik Dec 09 '17 at 20:52
  • @Galik but is that thread safe by itself? I don't think so. I assume thats why the `atomic_` methods are there – Curious Dec 09 '17 at 20:52
  • 1
    Anything that relates to the control bloc (the reference count) is atomic. So obtaining a new weak pointer is thread safe but not guaranteed to be pointing at anything. (if other threads have access to your shared pointer). – Galik Dec 09 '17 at 20:54
  • @Galik do you want to put that up as an answer? Along with a suggestion on how to achieve this? – Curious Dec 09 '17 at 20:58

2 Answers2

3

As soon as your shared pointer gets a new value, your weak pointer will start returning nullptr The reason being that the original object gets destroyed as soon as your shared pointer starts pointing to a different object.

But there will be no undefined behavior exchanging pointers because the handoff is atomic.

(Although, dereferencing the nullptr value shared pointer returned by w_ptr.lock() is undefined and will likely crash the program).

You would need to get a new weak pointer every time your shared pointer gets reset with a new value. But whether or not the shared pointer is still pointing at that new value by the time you lock() your weak pointer is anyone's guess.

Anything that relates to the control bloc (the reference count) is atomic. So obtaining a new weak pointer is thread safe but not guaranteed to be pointing at anything (if other threads have access to your shared pointer).

Galik
  • 47,303
  • 4
  • 80
  • 117
  • +1 on Galik's answer. `s_ptr = make_shared(move(new_value));` resulted in w_ptr becoming `nullptr`. But instead `*s_ptr = new_value;` resulted in w_ptr seeing the change, however that's running with mutating scissors (as previously discussed) when multithreaded. Need something additional to coordinate mutations across threads. – Eljay Dec 09 '17 at 21:06
1

This code can execute UB:

auto s_ptr = std::make_shared<T>(...);
auto w_ptr = std::weak_ptr<T>{s_ptr};

// thread one
while (...) { // A
  auto ptr = w_ptr.lock(); // B
  cout << "Value contained is " << *ptr << endl; // C
}

// thread two
while (...) {
  // do the heavy computation on the side
  auto new_value = fetch_new_value();

  // then "atomically" swap
  s_ptr = std::make_shared<T>(std::move(new_value)); // X
}

If thread two executed // X before thread one executed // B, weak_ptr no longer refers to any data.

.lock() then returns a "null" shared_ptr, which you proceed to dereference at // C.

If you guarded use of ptr against it being null, there would be no UB apparent.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Got it, I had originally misunderstood the relation between `shared_ptr` and `weak_ptr`. The relation is not with the `shared_ptr` object but with the control block.. I've just never used `weak_ptr` before and thought I had a good usecase for it.. – Curious Dec 09 '17 at 22:40