Skip to content
C++
Language
since C++98
Intermediate

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++98

A 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:

  1. No dead-store elimination — every write is emitted, even if the value is immediately overwritten
  2. No load caching — every read fetches from the memory location, not a register copy
  3. Relative ordering of volatile accesses is preserved within a single thread

What volatile does not guarantee:

  1. Atomicity — a volatile uint64_t on a 32-bit platform may still be two separate 32-bit operations
  2. Thread-safety — no happens-before relationship is established between threads
  3. 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

cpp
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 volatile

Member 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:

cpp
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.

cpp
#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:

cpp
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 order

const 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:

cpp
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:

cpp
#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:

cpp
#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.

Propertyvolatilestd::atomic<T> (C++11)
Suppresses register cachingYesYes (as a side effect)
Suppresses dead-store elisionYesYes
Indivisible read-modify-writeNoYes
Synchronization / happens-beforeNoYes
Compiler fenceNoYes
Hardware fence (where required)NoYes
Correct for MMIOYesGenerally no
Correct for inter-thread flagsNoYes
cpp
// 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-before

The rare case where both are needed — a hardware-atomic semaphore accessible via MMIO — volatile std::atomic<T> is valid C++11:

cpp
// 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);

Best Practices

  • Apply volatile to the pointee type, not the pointer itself: volatile uint32_t* reg (pointer to volatile), not uint32_t* volatile reg (volatile pointer to non-volatile — almost never what you want)
  • Use const volatile for hardware registers the software should never write
  • Group peripheral registers into volatile structs rather than scattering individual volatile variables across the codebase
  • In C++11 and later, use std::atomic for 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/write
  • std::atomic_signal_fence — compiler-only fence between signal handler and hosting thread (C++11)
  • const_cast — the only cast that can add or remove volatile qualification