History of the question. Prior to C++20, applying atomic operations to existing non-atomic objects required cumbersome workarounds, as std::atomic mandates that objects be constructed as atomic from inception. Programmers often attempted dangerous reinterpret_cast operations to treat plain objects as atomic, violating strict aliasing rules and invoking undefined behavior due to object lifetime mismatches. The introduction of std::atomic_ref in C++20 addressed this gap by providing a non-owning view that temporarily imbues atomic semantics onto existing objects without altering their storage type or lifetime.
The problem. std::atomic imposes specific representation requirements—such as lock-free bitflags or internal mutexes—that typically change the object's size or alignment compared to the underlying type T. Consequently, an object of type int is not layout-compatible with std::atomic<int>, making pointer punning impossible. Furthermore, std::atomic_ref requires that the referenced object satisfy stringent alignment constraints; specifically, the object's address must be aligned to at least alignof(std::atomic_ref<T>), which for many platforms equals alignof(T) but may be larger for hardware-specific atomic instructions. Violating this alignment precondition results in undefined behavior, potentially manifesting as torn reads or hardware exceptions on strict architectures like ARM.
The solution. std::atomic_ref acts as a lightweight wrapper holding a pointer to the target object, applying compiler intrinsics or hardware instructions to enforce atomicity without assuming the storage is a std::atomic instance. It respects the existing object's lifetime while providing the same memory ordering guarantees as std::atomic for the duration of each operation. To use it safely, developers must ensure the object is suitably aligned, typically through alignas specifiers or by verifying that std::atomic_ref<T>::required_alignment is satisfied, thereby enabling lock-free concurrent access to legacy data structures or C-compatible layouts.
#include <atomic> #include <cstdint> #include <iostream> struct alignas(alignof(std::atomic_ref<std::uint64_t>)) Data { std::uint64_t value; }; int main() { Data d{42}; std::atomic_ref<std::uint64_t> ref(d.value); ref.fetch_add(8, std::memory_order_relaxed); std::cout << d.value << " "; // Output: 50 }
Problem description. In a high-frequency trading application, a legacy C-struct defined the market feed packet layout, containing a double price field that needed atomic updates from the network thread while the strategy thread read it. The exchange mandated exact binary compatibility, preventing modification of the struct to use std::atomic<double>, and the latency requirements prohibited mutex locks or memory copies. We faced a data race where partial writes to the double (non-atomic on x86-64 without proper alignment) caused the strategy thread to read corrupted "ghost" values during high volatility spikes.
Different solutions considered. The first approach involved double-buffering with std::atomic<bool> flags, maintaining two copies of the struct and atomically flipping a pointer. While lock-free, this doubled memory consumption and introduced cache-line bouncing between NUMA nodes, degrading performance by approximately 15% in microbenchmarks. The second approach considered std::memcpy into a local std::atomic<double> variable, but this violated real-time constraints due to the extra copy and still suffered from torn reads if the memcpy occurred mid-update. The third solution utilized std::atomic_ref to directly reference the price field within the C-struct, leveraging hardware CAS (Compare-And-Swap) instructions without altering the struct layout.
Which solution was chosen and why. We selected std::atomic_ref because it provided true zero-overhead abstraction: the generated assembly on x86-64 was identical to hand-written lock cmpxchg instructions, with no additional allocations or indirection. Unlike the double-buffering approach, it maintained single-cache-line residency for the hot data, preserving L1 cache locality critical for microsecond-level latency. Crucially, it respected the ABI constraints of the external C library while eliminating data races through hardware-enforced atomicity.
The result. After implementation, the system achieved consistent lock-free updates with sub-microsecond latency, eliminating the ghost value anomalies verified through ThreadSanitizer runs. The alignment verification (alignas) ensured portability to ARM64 servers without code changes, and the throughput improved by 12% compared to the double-buffering baseline due to reduced cache pressure.
Why does casting a non-atomic pointer to std::atomic<T>* invoke undefined behavior when std::atomic_ref is safe?
Casting via reinterpret_cast creates a pointer to an object of type std::atomic<T>, but the storage actually contains an object of type T. This violates the C++ object model's strict aliasing rules and the lifetime requirements, as std::atomic<T> may have a different size, alignment, or internal state (like a spinlock) than T. std::atomic_ref is designed as a distinct reference type that explicitly refers to a T object and applies atomic operations to it through implementation-specific intrinsics, without pretending the storage is a different type, thus preserving the original object's lifetime and layout.
Does std::atomic_ref synchronize with the construction of the object it references?
No. std::atomic_ref provides atomicity only for operations performed through it, but it does not establish happens-before relationships with the constructor of the referenced object. If Thread A constructs an object and Thread B immediately creates an std::atomic_ref to it, Thread B might see uninitialized memory unless Thread A performed a release operation (e.g., storing to an std::atomic<bool>) and Thread B performed an acquire operation before accessing the atomic_ref. The atomic_ref itself assumes the object is already live and accessible, but concurrent non-atomic writes during construction remain data races without external synchronization.
Can std::atomic_ref be used with const objects, and what are the limitations?
Yes, std::atomic_ref<const T> is valid and permits atomic read operations (like load) on objects declared const, provided the object was not originally declared as const in a way that permits compiler optimizations to cache values in registers. However, you cannot construct an std::atomic_ref<T> (non-const) from a const T&, as this would violate const-correctness. Additionally, even with atomic_ref<const T>, the underlying object must not reside in read-only memory (e.g., .rodata section), as hardware atomic instructions require writable cache lines even for read operations on most architectures.