C++ Logo

std-discussion

Advanced search

Re: Lack of ordering guarantees in shared_ptr wording

From: Nate Eldredge <nate_at_[hidden]>
Date: Mon, 17 Feb 2025 21:11:41 -0700
> On Feb 16, 2025, at 06:40, Jennifier Burnett via Std-Discussion <std-discussion_at_[hidden]> wrote:
>
> Ah, ok to follow on from this the main thing that seems to have passed me by is this in Nate's response:
>
> >I presume that in the vast majority of cases, this happens-before is achieved simply by sequenced-before; within a single thread, with respect to program order, you access the object through a shared_ptr, then destroy that shared_ptr, and don't access the object again after that.
>
> How is sequenced-before equivalent to happens-before without any provisions of ordering?

Sequenced-before implies happens-before. It's right there in the definition: https://eel.is/c++draft/intro.races#10.1

> My understanding is that given the following:
>
> std::atomic<int>* foo = ...
> int* bar = ..
>
> int temp = *bar;
> foo->fetch_sub(1, std::memory_order_relaxed);
>
> There is nothing that requires the read from *bar to happen before the fetch_sub, because they're from two independent variables and the atomic operation provides no ordering constraints.

It is definitely true that the read from *bar happens-before the fetch_sub (see above). But it's also true that the read of *bar can be reordered with the fetch_sub.

It's tempting to think that "X happens-before Y" should mean something like "X must be observed before Y", or "X cannot be reordered with Y". Neither of those is true in general.

The way I think about it is that "X happens-before Y" is only *directly* observable information when X and Y are two operations on the *same* object. In that case, it means that Y observes X: following https://eel.is/c++draft/intro.races#13 for non-atomic objects, and p14-18 for atomic objects.

Having "X happens-before Y" for operations on two different objects isn't something you can directly observe; you can only use that information together with transitivity to indirectly learn something about a happens-before relationship between two operations on some other single object. And it so happens that those semantics are satisfied by applying the usual reordering rules: anything can reorder with a relaxed operation, no reordering after a release or before an acquire.

> However what you seem to be claiming (unless I'm misunderstanding, which I probably am) is that in the case of shared_ptr the fact that the read is sequenced-before the fetch_sub would be sufficient to prevent both the compiler and hardware from moving the read until after the fetch_sub. The thing I'm missing is exactly how that conclusion is being reached.

Right, sequenced-before alone does not prevent reordering. But sequenced-before together with a release barrier does. And that's the only way you can get a happens-before relationship between operations on different threads.

So the proposed wording, the promised (strongly) happens-before between each shared_ptr destructor and the deleter, which are potentially in different threads, can in practice only be achieved by having a release barrier on the reference counter decrement. And once that exists, then having your operation sequenced-before the shared_ptr destructor suffices.


Received on 2025-02-18 04:11:56