volatile
volatile prevents the compiler from caching or eliding accesses to memory that changes outside program control — essential for MMIO, not a substitute for std::atomic.
volatilesince C++98A type qualifier that forces the compiler to treat every read and write to the qualified object as an observable side effect — preventing register caching, dead-store elimination, and reordering of volatile accesses relative to one another within a single thread.
Overview
The compiler's optimizer assumes that memory it writes to only changes when the program explicitly writes to it. For hardware registers, DMA buffers, and signal-handler flags, that assumption is false. volatile breaks it: every access to a volatile object must appear in the emitted object code, in source order relative to other volatile accesses.
The C++ standard (since C++98) categorizes a volatile access as an observable side effect of the abstract machine. Side effects cannot be elided or reordered against other side effects in the same thread.
What volatile guarantees — all at the compiler level:
- No dead-store elimination — every write is emitted, even if the value is immediately overwritten
- No load caching — every read fetches from the memory location, not a register copy
- Relative ordering of volatile accesses is preserved within a single thread
What volatile does not guarantee:
- Atomicity — a
volatile uint64_ton a 32-bit platform may still be two separate 32-bit operations - Thread-safety — no happens-before relationship is established between threads
- Hardware memory ordering — on weakly-ordered architectures (ARM, POWER), the CPU can still reorder stores past other cores' loads absent explicit barrier instructions
The last point is decisive. volatile is entirely a compiler directive. It does not emit fence instructions. On x86 (Total Store Order), this is often invisible. On ARM or POWER, it is a correctness bug waiting to trigger.
Syntax
volatile int sensor; // volatile-qualified variable
volatile uint32_t* reg; // pointer to volatile uint32_t
const volatile uint32_t* status; // pointer to const volatile — read-only hardware register
uint32_t* volatile vptr; // volatile pointer (rarely useful)
const volatile uint32_t* const creg = ...; // const pointer to const volatileMember functions can be volatile-qualified, analogous to const member functions. A volatile member function may only be called through a volatile object, pointer, or reference:
struct Register {
volatile uint32_t value;
void write(uint32_t v) volatile { value = v; }
uint32_t read() const volatile { return value; }
};Examples
Memory-Mapped I/O
Hardware registers change state independently of the CPU — without volatile, the compiler may cache a register's value in a CPU register and never re-read from the actual peripheral address.
#include <cstdint>
// UART peripheral layout (STM32F4xx / ARM Cortex-M)
struct UART_t {
volatile uint32_t SR; // status — hardware clears bits on read/write
volatile uint32_t DR; // data — read to receive, write to transmit
volatile uint32_t BRR; // baud rate
volatile uint32_t CR1; // control 1
};
constexpr uintptr_t UART1_BASE = 0x4001'1000;
volatile UART_t* const UART1 = reinterpret_cast<UART_t*>(UART1_BASE);
constexpr uint32_t SR_TXE = 1u << 7; // TX buffer empty
constexpr uint32_t SR_RXNE = 1u << 5; // RX data available
void uart_send(char c) {
// Without volatile on SR, optimizer may hoist the read outside the loop
while (!(UART1->SR & SR_TXE)) {}
UART1->DR = static_cast<uint32_t>(c);
}
char uart_recv() {
while (!(UART1->SR & SR_RXNE)) {}
return static_cast<char>(UART1->DR & 0xFF); // read clears RXNE flag
}The double-write pattern shows dead-store prevention — some peripherals require a specific write sequence:
volatile uint32_t* ctrl = reinterpret_cast<volatile uint32_t*>(0x4000'0010);
*ctrl = 0x00; // without volatile: compiler sees a dead store and may elide this
*ctrl = 0xFF; // with volatile: both writes are emitted, in orderconst volatile — Read-Only Hardware Registers
Status and ID registers are written by hardware but should never be written by software. const volatile encodes both constraints, making an accidental write a compile error:
const volatile uint32_t* const CHIP_ID =
reinterpret_cast<const volatile uint32_t*>(0x1FFF'7A10);
uint32_t read_chip_id() {
return *CHIP_ID; // volatile: must actually load; const: write would not compile
}Signal Handlers
POSIX signals interrupt program flow asynchronously. Only volatile sig_atomic_t is guaranteed by the C standard to be safe for read/write from a signal handler without a data race:
#include <csignal>
#include <cstdio>
volatile std::sig_atomic_t g_interrupted = 0; // C++98 and later
extern "C" void handle_sigint(int /*sig*/) {
g_interrupted = 1; // sig_atomic_t assignment is atomic w.r.t. async signals
}
int main() {
std::signal(SIGINT, handle_sigint);
while (!g_interrupted) {
// do work — volatile prevents caching g_interrupted in a register
}
std::puts("shutting down");
}setjmp / longjmp
The standard specifies that local variables modified between setjmp and the longjmp that returns to it must be volatile to have defined values after the jump:
#include <csetjmp>
std::jmp_buf env;
void recover() {
volatile int step = 0; // value survives longjmp
int cached = 0; // value is indeterminate after longjmp — C++98 §18.7
if (setjmp(env) != 0) {
// step reliably holds the value assigned before longjmp
// cached may be anything
return;
}
step = 1;
// ... deep call stack that may longjmp ...
step = 2;
}volatile vs std::atomic
This is the most consequential distinction in modern C++. C++11 introduced std::atomic and a formal memory model that makes volatile's inadequacy for threading explicit in the standard.
| Property | volatile | std::atomic<T> (C++11) |
|---|---|---|
| Suppresses register caching | Yes | Yes (as a side effect) |
| Suppresses dead-store elision | Yes | Yes |
| Indivisible read-modify-write | No | Yes |
| Synchronization / happens-before | No | Yes |
| Compiler fence | No | Yes |
| Hardware fence (where required) | No | Yes |
| Correct for MMIO | Yes | Generally no |
| Correct for inter-thread flags | No | Yes |
// WRONG: volatile is not thread-safe — undefined behavior per C++11 memory model
volatile bool ready = false;
int shared = 0;
// Thread A
shared = 42;
ready = true; // compiler preserves order relative to other volatile accesses,
// but no barrier instruction — ARM hardware may reorder the store
// Thread B
while (!ready) {}
int x = shared; // may read 0 on ARM; data race is UB regardless of architecture
// CORRECT: std::atomic with explicit ordering (C++11)
std::atomic<bool> ready{false};
int shared = 0;
// Thread A
shared = 42;
ready.store(true, std::memory_order_release); // release barrier: shared visible before ready
// Thread B
while (!ready.load(std::memory_order_acquire)) {}
int x = shared; // guaranteed to observe 42 — acquire/release establishes happens-beforeThe rare case where both are needed — a hardware-atomic semaphore accessible via MMIO — volatile std::atomic<T> is valid C++11:
// Hardware-atomic test-and-set register at a fixed physical address
volatile std::atomic<uint32_t>* hw_sem =
reinterpret_cast<volatile std::atomic<uint32_t>*>(0x4000'FF00);
uint32_t expected = 0;
hw_sem->compare_exchange_strong(expected, 1u, std::memory_order_acquire);DMA Buffer Management
Direct Memory Access (DMA) is a hardware mechanism that transfers data between peripherals and main memory without CPU involvement. Because the DMA controller writes to memory independently of the CPU executing program code, the compiler has no visibility into those writes — and may freely cache the buffer's contents in registers or eliminate reads it considers redundant. volatile prevents both optimizations.
#include <cstdint>
// DMA receive buffer mapped at a fixed physical address (C++98)
// Attribute tells the linker to place the symbol at the hardware-assigned address
volatile uint8_t dma_buf[512]
__attribute__((section(".dma_section"))); // GCC/Clang embedded toolchain
// DMA completion flag — hardware sets this to 1 when the transfer is done
constexpr uintptr_t DMA_COMPLETE_FLAG_ADDR = 0x2001'0000;
volatile uint32_t* const dma_complete_flag =
reinterpret_cast<volatile uint32_t*>(DMA_COMPLETE_FLAG_ADDR);
// Poll until the DMA controller signals completion
void wait_for_dma() {
// Without volatile on dma_complete_flag, the compiler may load the value once,
// find it 0, and spin forever on that cached register value.
while (*dma_complete_flag == 0) {
// spinloop — real firmware would yield or enter WFI (Wait For Interrupt)
}
*dma_complete_flag = 0; // acknowledge: clear the flag before processing
}
void process_dma_transfer() {
wait_for_dma();
for (std::size_t i = 0; i < 512; ++i) {
// Without volatile on dma_buf, the compiler may reorder or cache these reads
uint8_t byte = dma_buf[i]; // each access must load from memory
(void)byte;
// ... process byte ...
}
}The volatile qualifier on dma_buf ensures every element access goes to actual memory, where the DMA hardware wrote the data. Without it the compiler may read the buffer once, cache values in registers, and process stale data even after the DMA transfer has updated memory.
Best Practice: declare DMA buffers as volatile arrays and gate all processing behind a volatile completion flag. Many embedded platforms also require the buffer to be cache-aligned and placed in a non-cacheable memory region via linker script — volatile handles the compiler side, but the memory subsystem configuration is platform-specific.
Cache Coherency on Multi-Core Systems
On multi-core processors with ARM or POWER architectures, each core has private L1 and L2 caches. A store issued by Core 0 may sit in Core 0's cache indefinitely, invisible to Core 1, even when the location is volatile. The volatile qualifier tells the compiler to emit the load or store instruction; it says nothing about when that store propagates through the cache hierarchy to other cores.
// C++11 — illustrating the hardware gap that volatile alone cannot close
volatile int data = 0;
volatile bool ready = false;
// Core 0 thread
void producer() {
data = 42;
ready = true;
// On ARM: these stores are emitted by the compiler (volatile guarantees that),
// but the hardware may not have flushed the cache lines to the point of
// coherency before Core 1 observes ready == true.
}
// Core 1 thread — may observe ready=true while data is still 0 in its cache
void consumer() {
while (!ready) {}
int v = data; // v may be 0 — a hardware reordering, not a compiler bug
}Fixing this requires explicit memory barrier instructions. On ARM these are the Data Synchronization Barrier (DSB) and the Instruction Synchronization Barrier (ISB):
// ARM barrier intrinsics — GCC/Clang, AArch32 and AArch64 (C++11)
void arm_full_barrier() {
// "dsb sy" — Data Synchronization Barrier, full system domain.
// Forces all preceding memory accesses to complete before any subsequent
// memory access is issued on this core; also drains the store buffer.
__asm__ volatile ("dsb sy" ::: "memory"); // "memory" clobber: compiler fence too
// "isb" — Instruction Synchronization Barrier.
// Flushes the pipeline; required after MMU/cache configuration changes.
// Rarely needed for data ordering alone; shown for completeness.
__asm__ volatile ("isb" ::: "memory");
}The portable C++11 equivalent uses the standard atomic fence API, which the compiler lowers to the correct barrier instruction for the target architecture:
#include <atomic>
// C++11 — portable full memory barrier
void portable_barrier() {
// seq_cst fence: prevents the compiler and hardware from reordering any
// memory operation across this point in either direction.
std::atomic_thread_fence(std::memory_order_seq_cst);
}
// Corrected producer/consumer using portable barriers (C++11)
volatile int data_v = 0;
volatile bool ready_v = false;
void producer_safe() {
data_v = 42;
std::atomic_thread_fence(std::memory_order_release); // drain stores before flag
ready_v = true;
}
void consumer_safe() {
while (!ready_v) {}
std::atomic_thread_fence(std::memory_order_acquire); // complete loads after flag
int v = data_v; // now guaranteed to see 42 on ARM and POWER
}The distinction is crucial: volatile prevents the compiler from optimizing; memory barriers force the hardware to flush caches and respect the ordering constraint. Both layers are required for correct multi-core communication. In practice, prefer std::atomic over this manual approach — it combines both guarantees automatically.
Common Pitfall: assuming x86 behavior validates ARM correctness. x86's Total Store Order model means stores are globally visible nearly immediately, masking the bug entirely during desktop testing. The same code running on an ARM-based server or embedded SoC will fail intermittently under load.
volatile in C++20 — Deprecated Uses
C++20 (P1152) deprecated several uses of volatile whose semantics were unclear or whose implementation-defined behavior made them unreliable across compilers. The deprecated operations still compile — typically with a -Wvolatile or /W4 warning — but their removal is scheduled for a future standard revision.
Deprecated: volatile compound assignment operators. The read-modify-write expressed by +=, -=, |=, and similar operators is not atomic. The standard never guaranteed the load and store would not be separated by an interrupt or a context switch. C++20 deprecated these operators on volatile-qualified objects because their apparent atomicity is misleading:
volatile int counter = 0;
// C++20 DEPRECATED — emits -Wvolatile; misleadingly looks atomic
counter += 1; // deprecated: volatile compound assignment
counter++; // deprecated: volatile increment
// C++20 REPLACEMENT — explicit load, modify, store; intent is clear
int tmp = counter; // load
tmp += 1; // modify
counter = tmp; // store
// For true atomicity, use std::atomic<int> insteadDeprecated: volatile bit-fields. Accessing a bit-field through a volatile-qualified object was deprecated because the mapping from bit-field to memory accesses is implementation-defined. Two compilers may emit entirely different read-modify-write sequences for the same bit-field assignment:
struct Flags {
volatile unsigned int enable : 1; // C++20 DEPRECATED
volatile unsigned int mode : 3; // C++20 DEPRECATED
};
// Replacement: use a volatile integer with explicit bitmask operations
volatile uint32_t flags_reg = 0;
constexpr uint32_t ENABLE_BIT = 1u << 0;
constexpr uint32_t MODE_MASK = 0x7u << 1;
flags_reg = (flags_reg & ~ENABLE_BIT) | ENABLE_BIT; // set enable bit — C++98Deprecated: volatile in structured bindings. C++17 structured bindings with a volatile-qualified initializer were deprecated in C++20 because the cv-qualification rules for bindings were underspecified:
volatile int arr[2] = {1, 2};
// C++20 DEPRECATED
auto& [a, b] = arr; // volatile structured binding — deprecated
// Replacement: access elements explicitly
volatile int& va = arr[0];
volatile int& vb = arr[1];The non-deprecated subset of volatile — qualifying individual variables, pointer targets, member function signatures, and signal-handler flags — remains valid and unchanged in C++20 and C++23.
Windows MemoryBarrier and Compiler Barrier Intrinsics
On Windows, the WDK (Windows Driver Kit) and MSVC provide a set of barrier intrinsics that cover both compiler and hardware fences. Understanding when to use each is important for kernel-mode driver code versus user-mode code:
#include <windows.h> // for MemoryBarrier, _ReadBarrier, _WriteBarrier
#include <intrin.h> // for _ReadWriteBarrier (MSVC compiler barrier)
// ---- Full hardware fence ----
// MemoryBarrier() — emits MFENCE on x86-64, DMB ISH on ARM.
// Prevents both the compiler AND the CPU from reordering memory accesses across
// this point. Use in kernel-mode driver code when ordering MMIO relative to DMA,
// and in user-mode code when synchronizing with a device via shared memory.
void full_fence_example() {
volatile uint32_t* reg = reinterpret_cast<volatile uint32_t*>(0xFED00000);
*reg = 0x1;
MemoryBarrier(); // hardware fence — store to reg is globally visible before
*reg = 0x2; // this subsequent write
}
// ---- Compiler-only barriers (no hardware fence instruction emitted) ----
// Use these only when the CPU's memory model is already strong enough
// (e.g., x86 TSO) and you merely need to prevent compiler reordering.
void compiler_barrier_example() {
volatile uint32_t* flag = reinterpret_cast<volatile uint32_t*>(0xFED00004);
_ReadBarrier(); // compiler: do not move loads past this point
uint32_t v = *flag; // load
_WriteBarrier(); // compiler: do not move stores past this point
*flag = v | 0x80; // store
_ReadWriteBarrier(); // compiler: do not move any memory op past this point
}The table below summarizes the Windows barrier primitives:
| Function | Hardware fence | Compiler fence | Suitable for |
|---|---|---|---|
MemoryBarrier() | Yes (full) | Yes | Kernel drivers, MMIO, shared-memory IPC |
_ReadBarrier() | No | Yes (loads) | x86-only user-mode; avoid in portable code |
_WriteBarrier() | No | Yes (stores) | x86-only user-mode; avoid in portable code |
_ReadWriteBarrier() | No | Yes (all) | x86-only; full compiler fence, no CPU fence |
Best Practice: prefer MemoryBarrier() in kernel-mode driver code — compiler-only barriers (_ReadBarrier, _WriteBarrier, _ReadWriteBarrier) do not emit CPU fence instructions and are insufficient on ARM-based Windows hardware (Surface Pro X, Windows on ARM servers). For portable kernel code targeting both x86 and ARM, use MemoryBarrier() or the KeMemoryBarrier() kernel API, which resolves to the correct instruction per architecture.
In user-mode code that targets only x86-64 Windows, the compiler barriers are sometimes used around volatile accesses to avoid the overhead of MFENCE. This is a micro-optimization; std::atomic_thread_fence from the C++11 standard library is the portable equivalent.
volatile Member Functions
A member function qualified volatile may be called on a volatile-qualified object, pointer, or reference — exactly as a const member function may be called on a const-qualified object. The two qualifiers are orthogonal and may be combined.
This enables cv-qualifier overloading: providing one overload for normal (non-volatile) access to configuration registers and a separate overload for volatile access to status registers that change under hardware control. The compiler selects the correct overload based on the cv-qualification of the object through which the call is made.
#include <cstdint>
// Hardware timer peripheral (C++98)
// Configuration registers: written once at init, never change during operation.
// Status registers: updated by hardware every tick — must be volatile.
class HardwareTimer {
public:
// Non-volatile overload — called on ordinary (non-volatile) HardwareTimer
// objects to read or write configuration registers.
void set_period(uint32_t ticks) { config_.period = ticks; }
uint32_t get_period() const { return config_.period; }
// volatile overload — called when the object itself is volatile-qualified.
// Reads the live counter value, which the hardware updates every tick.
// 'const volatile' means: read-only from software's perspective,
// but the hardware may change it between two calls.
uint32_t read_counter() const volatile { return status_.counter; }
bool is_running() const volatile { return status_.running != 0; }
// A function that needs both: reconfigure (non-volatile path) after
// verifying the timer is stopped (volatile path).
void reset_period(uint32_t new_period) volatile {
// 'this' is volatile here, so only volatile-qualified members accessible
while (is_running()) {} // spin on volatile status register
// Cast away volatile for the configuration write — safe only because
// config_ is not written by hardware; only software touches it.
const_cast<HardwareTimer*>(this)->set_period(new_period);
}
private:
struct Config {
uint32_t period = 0;
uint32_t prescaler = 0;
} config_;
struct Status {
volatile uint32_t counter; // live tick counter — hardware writes this
volatile uint32_t running; // 1 if timer is active
} status_;
};
// Usage
volatile HardwareTimer* timer =
reinterpret_cast<volatile HardwareTimer*>(0x4000'0000);
uint32_t ticks = timer->read_counter(); // calls const volatile overload — correct
// timer->set_period(1000); // would not compile: set_period is not volatileThe const_cast inside reset_period is safe specifically because config_ is a pure-software structure — only the program writes to it. Applying const_cast to genuinely hardware-written memory (like status_.counter) and then writing through the result is undefined behavior.
Best Practice: declare hardware peripheral structs with volatile on status fields and ordinary types for configuration fields, rather than making the entire struct volatile. This gives the compiler more optimization freedom over configuration data while ensuring every status read is genuinely emitted.
Clearing Sensitive Memory
When a function holds sensitive data (passwords, cryptographic keys, private keys) in a local buffer, the compiler's dead-store elimination may remove the memset call that zeroes it out — because the optimizer can prove the buffer is never read after the zeroing:
void insecure_clear(char* buf, std::size_t n) {
std::memset(buf, 0, n); // optimizer MAY elide this entirely if buf is
// not read again before it goes out of scope
}Several platform-specific and standard functions solve this correctly by either being declared in a way the optimizer cannot see through, or by using side-effect semantics the standard guarantees will not be elided.
#include <cstring> // memset_s (C11 Annex K, available on some C++ implementations)
#include <algorithm> // std::fill
// ---- Option 1: memset_s (C11 Annex K / C++17 Annex K if adopted) ----
// The standard specifies memset_s as a "runtime constraint" function whose
// side effects the implementation must not elide. Not universally available
// (glibc requires _GNU_SOURCE or C11 mode; MSVC provides it).
void clear_with_memset_s(char* buf, std::size_t n) {
memset_s(buf, n, 0, n); // C11 Annex K — optimizer must not elide
}
// ---- Option 2: explicit_bzero (POSIX.1-2024) ----
// Designed specifically for security-sensitive zeroing. The standard mandates
// it is not elided. Available on Linux (glibc 2.25+), macOS (10.12+), OpenBSD.
#if defined(__linux__) || defined(__APPLE__) || defined(__OpenBSD__)
void clear_with_explicit_bzero(void* buf, std::size_t n) {
explicit_bzero(buf, n); // POSIX.1-2024 — never elided
}
#endif
// ---- Option 3: SecureZeroMemory (Windows) ----
// Windows SDK equivalent — guaranteed by MSVC and the Windows ABI not to be
// optimized away. Use in both user-mode and kernel-mode Windows code.
#if defined(_WIN32)
#include <windows.h>
void clear_with_windows(void* buf, std::size_t n) {
SecureZeroMemory(buf, n); // Windows SDK — never elided
}
#endif
// ---- Option 4: volatile pointer cast idiom (portable C++98) ----
// Writing through a volatile pointer forces the compiler to emit each store.
// This is the idiom used in many embedded and cross-platform codebases where
// the platform functions above are unavailable.
void volatile_clear(void* buf, std::size_t n) {
volatile char* p = static_cast<volatile char*>(buf); // C++98
for (std::size_t i = 0; i < n; ++i) {
p[i] = 0; // volatile write — optimizer must emit each store
}
}
// ---- Portable secure_clear helper (C++11) ----
// Falls through platform APIs with the volatile idiom as a universal fallback.
void secure_clear(void* buf, std::size_t n) noexcept { // C++11
#if defined(_WIN32)
SecureZeroMemory(buf, n);
#elif defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 25))
explicit_bzero(buf, n);
#elif defined(__APPLE__) || defined(__OpenBSD__)
explicit_bzero(buf, n);
#else
// Fallback: volatile pointer cast — correct but no standard guarantee;
// works on all known compilers as of C++23.
volatile unsigned char* p = static_cast<volatile unsigned char*>(buf);
for (std::size_t i = 0; i < n; ++i) {
p[i] = 0u;
}
#endif
}The mechanism behind the volatile pointer idiom is the same as for MMIO: the standard treats each write to a volatile object as an observable side effect. The optimizer is not permitted to elide observable side effects, so all n zero-stores must be emitted. In practice every major compiler (GCC, Clang, MSVC) respects this, though the standard's own wording does not make a stronger guarantee for this specific use case than it does for any other volatile write.
Common Pitfall: using a plain volatile local variable instead of a volatile pointer cast. The distinction matters: volatile char buf[N]; memset(buf, 0, N); does not help — memset receives a void* (non-volatile), and the optimizer may still elide the call. The volatile qualifier must be present at the actual write site. The portable idiom above ensures this by operating through volatile unsigned char* directly.
Best Practice: use explicit_bzero or SecureZeroMemory whenever the target platform provides them — they are specified with explicit non-elision guarantees and require no reasoning about compiler behavior. Reserve the volatile pointer idiom for platforms where neither is available.
Best Practices
- Apply
volatileto the pointee type, not the pointer itself:volatile uint32_t* reg(pointer to volatile), notuint32_t* volatile reg(volatile pointer to non-volatile — almost never what you want) - Use
const volatilefor hardware registers the software should never write - Group peripheral registers into
volatilestructs rather than scattering individual volatile variables across the codebase - In C++11 and later, use
std::atomicfor any flag or value shared between threads — even on x86, it is UB to rely on volatile for this - Prefer
std::atomic_signal_fence(std::memory_order_seq_cst)(C++11) over volatile as a compiler barrier in signal-handler-adjacent code
Common Pitfalls
Assuming volatile implies atomicity. On a 32-bit ARM reading a volatile uint64_t may emit two separate LDR instructions. A concurrent writer between them produces a torn read. Only std::atomic<uint64_t> guarantees indivisibility.
Working fine on x86, breaking on ARM. x86's Total Store Order memory model masks volatile's thread-safety deficiencies. Code that uses volatile for inter-thread flags frequently passes all x86 tests and fails deterministically on ARM or POWER under load. This is undefined behavior in C++11 regardless of the platform.
Casting away volatile with const_cast. The syntax is legal, but writing through the resulting non-volatile pointer to an object that is genuinely volatile (e.g., an MMIO register) is undefined behavior — and the compiler may elide the write entirely once the volatile qualifier is gone.
Over-relying on volatile for secure memory erasure. Writing volatile to a local buffer before it goes out of scope prevents the compiler from eliding the stores in practice, but the standard's guarantee is thin. For clearing sensitive memory, prefer explicit_bzero (POSIX.1-2024), SecureZeroMemory (Windows), or writing through a volatile pointer cast of the buffer address specifically to defeat the optimizer with documented intent.
See Also
std::atomic— lock-free atomic operations with configurable memory ordering (C++11)std::memory_order— synchronization semantics for atomic operations (C++11)std::sig_atomic_t— integer type guaranteed safe for signal handler read/writestd::atomic_signal_fence— compiler-only fence between signal handler and hosting thread (C++11)const_cast— the only cast that can add or removevolatilequalification