Assigning pointers to atomic type to pointers to non atomic type

C11 allows _Atomic T to have a different size and layout than T, e.g. if it's not lock-free. (See @PSkocik's answer).

For example, the implementation could choose to put a mutex inside each atomic object, and put it first. (Most implementations instead use the address as an index into a table of locks: Where is the lock for a std::atomic? instead of bloating each instance of an _Atomic or std::atomic<T> object that isn't guaranteed lock-free at compile time).

Therefore _Atomic T* is not compatible with T* even in a single-threaded program.

Merely assigning a pointer might not be UB (sorry I didn't put on my language lawyer hat), but dereferencing certainly can be.

I'm not sure if it's strictly UB on implementations where _Atomic T and T do share the same layout and alignment. Probably it violates strict aliasing, if _Atomic T and T are considered different types regardless of whether or not they share the same layout.


alignof(T) might be different from alignof(_Atomic T), but other than an intentionally perverse implementation (Deathstation 9000), _Atomic T will be at least as aligned as plain T, so that's not an issue for casting pointers to objects that already exist. An object that's more aligned than it needs to be is not a problem, just a possible missed-optimization if it stops a compiler from using a single wider load.

Fun fact: creating an under-aligned pointer is UB in ISO C, even without dereference. (Most implementations don't complain, and Intel's _mm_loadu_si128 intrinsic even requires compilers to support doing so.)


In practice on real implementations, _Atomic T* and T* use the same layout / object representation and alignof(_Atomic T) >= alignof(T). A single-threaded or mutex-guarded part of a program could do non-atomic access to an _Atomic object, if you can work around the strict-aliasing UB. Maybe with memcpy.

On real implementations, _Atomic may increase the alignment requirement, e.g. a struct {int a,b;} on most ABIs for most 64-bit ISAs would typically only have 4-byte alignment (max of the members), but _Atomic would give it natural alignment = 8 to allow loading/storing it with a single aligned 64-bit load/store. This of course doesn't change the layout or alignment of the members relative to the start of the object, just the alignment of the object as a whole.


The problem with all that is that applying the rules above we can also conclude that simple assignment a non-atomic type to an atomic type is also well defined which is obviously not true since we have a dedicated generic atomic_store function for that.

No, that reasoning is flawed.

atomic_store(&my_atomic, 1) is equivalent to my_atomic=1;. In the C abstract machine, they both do an atomic store with memory_order_seq_cst.

You can also see this from looking at the code-gen for real compilers on any ISA; e.g. x86 compilers will use an xchg instruction, or mov+mfence. Similarly, shared_var++ compiles to an atomic RMW (with mo_seq_cst).

IDK why there's an atomic_store generic function. Maybe just for contrast / consistency with atomic_store_explicit, which lets you do atomic_store_explicit(&shared_var, 1, memory_order_release) or memory_order_relaxed to do a release or relaxed store instead of sequential-release. (On x86, just a plain store. Or on weakly-ordered ISAs, some fencing but not a full barrier.)


For types that are lock-free, where the object representation of _Atomic T and T are identical, there's no problem in practice accessing an atomic object through a non-atomic pointer in a single-threaded program. I suspect it's still UB, though.

C++20 is planning to introduce std::atomic_ref<T> which will let you do atomic operations on a non-atomic variable. (With no UB as long as no threads are potentially doing non-atomic access to it during the time window of being written.) This is basically a wrapper around the __atomic_* builtins in GCC for example, that std::atomic<T> is implemented on top of.

(This presents some problems, like if atomic<T> needs more alignment than T, e.g. for long long or double on i386 System V. Or a struct of 2x int on most 64-bit ISAs. You should use alignas(_Atomic T) T foo when declaring non-atomic objects you want to be able to do atomic operations on.)

Anyway, I'm not aware of any standards-compliant way to do similar things in portable ISO C11, but it's worth mentioning that real C compilers very much do support doing atomic operations on objects declared without _Atomic. But only using stuff like GNU C atomic builtins.:

See Casting pointers to _Atomic pointers and _Atomic sizes : apparently casting a T* to _Atomic T* is not recommended even in GNU C. Although we don't have a definitive answer that it's actually UB.


6.2.5p27:

Further, there is the _Atomic qualifier. The presence of the _Atomic qualifier designates an atomic type. The size, representation, and alignment of an atomic type need not be the same as those of the corresponding unqualified type. Therefore, this Standard explicitly uses the phrase ''atomic, qualified or unqualified type'' whenever the atomic version of a type is permitted along with the other qualified versions of a type. The phrase ''qualified or unqualified type'', without specific mention of atomic, does not include the atomic types.

I think this should make it clear that atomic-qualified types are not deemed compatible with qualified or unqualified versions of the types they're based on.