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.
atomic_ref
doesn't make concurrent access safe. I fail to see the safety you are after. – Catalasestd::atomic_ref
out of it either. – Odoaceratomic_ref
. – Nomographyatomic_ref
is much simpler and, I believe, understandable. – Nomography