Reputation: 314
I'm looking for an alternative to std::atomic_ref
which might be used without needing C++20 support. I've considered casting pointers to std::atomic
but that does not seem like a safe option.
The use-case would be to apply atomic operations to non-atomic objects for the lifetime of the atomic reference to avoid race. The objects accessed cannot all be made atomic, so a wrapper like the atomic_ref would be needed.
Any help is appreciated!
Upvotes: 5
Views: 2599
Reputation: 364220
In GCC/clang (and other compilers that implement GNU C extensions), you can use the __atomic
builtins, such as
int load_result = __atomic_load_n(&plain_int_var, __ATOMIC_ACQUIRE);
That's how atomic_ref<T>
is implemented on such compilers: just wrappers for those builtins. (That's why atomic_ref
is super light-weight, and it's normally best to construct one for free every time you need one, not keep around a single atomic_ref
.)
Since you won't have std::atomic_ref<T>::required_alignment
, it's normally sufficient to give the objects natural alignment, i.e. alignas( sizeof(T) ) T foo;
to make sure __atomic
operations are actually atomic, as well as having memory-order guarantees. (On many implementations, all plain T
that support lock-free atomics at all already get sufficient alignment, but for example some 32-bit systems only align int64_t
by 4 bytes, but 8-byte atomics are only atomic with 8-byte alignment. x86 gcc -m32
had a problem with this in C++ for a while, and for a lot longer with _Atomic
in C, finally fixed in 2020, although it only affected struct members.)
reinterpret_cast< std::atomic<T>* >
may actually work in practice on most compilers, maybe not even being UB depending on the internals of atomic<>
.
(Most?) other compilers implement atomic (and atomic_ref) in a way that's similar to GNU C, I think, using builtin functions. e.g. for MSVC, something like _InterlockedExchange()
to implement atomic<>::exchange
.
In mainstream C++ implementations, atomic<T>
has the same size and layout as a plain T
. (The size is something you can static_assert
) It's in theory possible for a non-lock-free atomic<>
to include a mutex or something, but normal implementations don't (Where is the lock for a std::atomic?). (Partly for compat with C11 _Atomic
, which IIRC has some requirements about even uninitialized or maybe zero-initialized objects still working properly. But also just for size reasons.)
Despite ISO C++ not guaranteeing that it's well-defined, you will basically end up calling __atomic_fetch_add_n
or InterlockedAdd
on an int
member var of atomic<int>
with the same address as your original plain int
.
That might still technically be UB; there's a rule about structs being compatible up to the first difference in their definition, but I'm less sure about an int*
into a struct or especially a struct{int;}*
pointer to an int
object. I think that violates the strict-aliasing rule.
But I think still unlikely to break in practice. Still, the possible breakage would only show up under optimization, and be dependent on surrounding code, meaning it's not something you can easily write a unit-test for.
However, the most likely-to-break scenario would be if the same function (after inlining) was reading or writing the plain variable mixed with operations on the same variable through an atomic<>*
or atomic<>&
reference. Especially if there isn't any kind of memory barrier separating those accesses, such as calling some_thread.join()
. If you mixing atomic and non-atomic access within one function (after inlining), this may be safe and portable enough to work until you can use atomic_ref<>
properly.
The other good short-term option is manually using either GNU C or MSVC atomic builtins directly, if your source code currently only cares about one or the other. Or roll your own (limited subset of) atomic_ref
using the versions of these functions you actually need.
_InterlockedAdd
(the intrinsic/built-in function) https://learn.microsoft.com/en-us/previous-versions/51s265a6(v=vs.85)InterlockedAdd
(the Windows library function) https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-interlockedaddUpvotes: 4
Reputation: 23497
If your compiler supports OpenMP (most of them do), you can mark your object access with #pragma atomic
. Possibly with a proper operation (read
, write
, update
, capture
) and memory-ordering semantics.
EDIT
Alternatively, it also seems that Boost provides atomic_ref
available to pre-C++20 codes: https://www.boost.org/doc/libs/1_75_0/doc/html/atomic/interface.html#atomic.interface.interface_atomic_ref
Another way might be casting a non-atomic object into an atomic one by using reinterpret_cast
. This solution will likely cause undefined behavior, but may actually work with some implementations. It is, for instance, used in the Facebook Folly library: https://github.com/facebook/folly/blob/master/folly/synchronization/PicoSpinLock.h#L95.
Upvotes: 2