Question
What is the purpose of the volatile keyword in C++? I have rarely needed to use it, so I would like to understand what it does, why developers use it, and when it is appropriate. I have found explanations focused on C# and Java; how does volatile work in C++?
Short Answer
By the end of this page, you will understand that C++ volatile tells the compiler that a value can change outside ordinary program flow. You will know its appropriate low-level uses, such as memory-mapped hardware, and why it does not make normal multithreaded code safe.
Concept
volatile is a type qualifier in C++. It tells the compiler that each read from, and write to, an object is an observable operation that must not simply be removed or combined with another access during optimization.
Normally, a compiler may optimize this:
int status = 0;
while (status == 0) {
// The compiler can see no code here changes status.
}
Because nothing in this program appears to modify status, the compiler may assume the loop never ends. That assumption is often correct for ordinary memory.
However, some locations can change for reasons outside the current C++ code, such as a register controlled by a hardware device. Declaring such an object volatile tells the compiler not to treat repeated accesses as interchangeable:
volatile unsigned int* const deviceStatus =
reinterpret_cast<volatile unsigned int*>(0x40000000);
while ((*deviceStatus & 0x01u) == 0u) {
// Read the hardware register again on every iteration.
}
What volatile does not do
In modern C++, volatile is not a thread-synchronization tool. It does not make an operation atomic, prevent data races, provide mutual exclusion, or establish the ordering needed between threads.
For shared data accessed by multiple threads, use:
std::atomicfor simple shared state such as flags and counters.std::mutexto protect multi-step operations or related data.
This distinction matters because code may appear to work in testing but still have undefined behavior when threads access ordinary shared data without synchronization.
Mental Model
Think of ordinary variables as notes on your desk. If you read the same note twice and nobody in the room can change it, you may safely reuse what you read the first time.
A volatile variable is like a display connected to a machine outside the room. The displayed value may change at any moment, even though nobody in your local code changed it. Therefore, each time you look, the compiler must perform a fresh read instead of relying on an earlier observation.
This does not mean two people can safely edit the same notebook at once. For that, you need coordination: std::atomic operations or a std::mutex.
Syntax and Examples
Use volatile before the type, or after it:
volatile int sensorValue = 0;
int volatile anotherSensorValue = 0;
It is commonly used through a pointer when a fixed address represents a hardware register:
#include <cstdint>
constexpr std::uintptr_t statusRegisterAddress = 0x40000000;
auto statusRegister =
reinterpret_cast<volatile std::uint32_t*>(statusRegisterAddress);
std::uint32_t status = *statusRegister; // A volatile read.
*statusRegister = 1u; // A volatile write.
The pointer points to a location supplied by a device or platform. The volatile qualification tells the compiler that reading *statusRegister has an observable effect and must occur when the program performs that expression.
You can also qualify individual members:
class Device {
public:
std::uint32_t readStatus() volatile {
return status;
}
private:
std::uint32_t status;
};
A volatile member function can be called on a volatile Device object. This is mostly useful in specialized low-level code.
Step by Step Execution
Consider a simplified hardware-ready flag:
volatile bool deviceReady = false;
void waitForDevice() {
while (!deviceReady) {
// Wait until external hardware changes the value.
}
}
Step by step:
deviceReadyis declaredvolatile, meaning it might change outside normal C++ statements.waitForDevice()evaluates!deviceReady.- If the value is
false, the loop body runs. - When control returns to the condition, C++ must perform another volatile read of
deviceReady. - Once external hardware has made the value
true, the condition becomes false and the loop ends.
In a real hardware program, the object would usually represent a device-provided register rather than a normal global bool.
For threads, use an atomic flag instead:
#include <atomic>
std::atomic<bool> ready{false};
void worker() {
ready.store(true, std::memory_order_release);
}
void waitForWorker() {
while (!ready.load(std::memory_order_acquire)) {
}
}
The atomic version provides the synchronization relationship that volatile does not.
Real World Use Cases
Typical valid uses of C++ volatile are low-level and platform-specific:
- Memory-mapped I/O: Embedded programs read and write device registers at known memory addresses.
- Hardware status registers: A network controller, timer, serial port, or sensor can update a register independently of the CPU's normal code.
- Special platform interfaces: Some operating-system or compiler APIs require a
volatile-qualified address or object. - Limited signal-handler communication: In carefully constrained cases,
volatile std::sig_atomic_tmay be used to store a signal flag. Signal handling has strict rules, so platform and standard-library documentation still matter.
Most application-level C++ code—web services, desktop applications, games, and command-line tools—rarely needs volatile directly.
Real Codebase Usage
In production code, isolate volatile behind a small hardware abstraction rather than exposing it throughout the application.
#include <cstdint>
class Uart {
public:
bool canTransmit() const {
return (registers_->status & transmitEmptyMask) != 0u;
}
void writeByte(std::uint8_t byte) {
while (!canTransmit()) {
}
registers_->data = byte;
}
private:
struct Registers {
volatile std::uint32_t status;
volatile std::uint32_t data;
};
static constexpr std::uint32_t transmitEmptyMask = 0x01u;
Registers* const registers_;
};
This pattern keeps hardware-specific behavior in one place and gives the rest of the program ordinary methods such as canTransmit() and writeByte().
For concurrent application code, developers usually use a guard clause plus synchronization:
#include <atomic>
std::atomic<bool> shuttingDown{false};
void processRequest() {
if (shuttingDown.load()) {
return;
}
// Safely continue with request processing.
}
If several values must stay consistent together, a mutex is generally more suitable than several volatile or atomic variables.
Common Mistakes
Using volatile for thread communication
This is incorrect:
volatile bool finished = false;
int result = 0;
// Thread A
result = 42;
finished = true;
// Thread B
while (!finished) {
}
std::cout << result;
volatile does not prevent a data race and does not guarantee that Thread B observes result correctly. Use std::atomic with suitable memory ordering, or protect both variables with a std::mutex.
Assuming volatile makes count++ safe
volatile int count = 0;
count++; // Not atomic
Incrementing consists of reading, modifying, and writing. Concurrent increments can be lost. Use std::atomic<int> when an atomic counter is appropriate.
Adding volatile to ordinary variables "just in case"
This can reduce optimization opportunities and hides the real design issue. Only use it when a value truly represents externally changing memory or an API requires it.
Casting away volatile
volatile int registerValue = 0;
int* ordinaryPointer = const_cast<int*>(®isterValue); // Avoid this.
Accessing hardware-like memory through a non-volatile pointer can let the compiler optimize away accesses that must occur. Preserve the qualification through the code that performs those accesses.
Expecting it to be a portable hardware driver
volatile expresses compiler access behavior, but device addresses, register layouts, required barriers, and access widths are platform-specific. Follow the hardware and toolchain documentation.
Comparisons
| Tool or qualifier | Main purpose | Safe for thread synchronization? | Typical use |
|---|---|---|---|
volatile | Preserves observable reads/writes to externally changing memory | No | Memory-mapped device registers |
std::atomic<T> | Atomic access and inter-thread synchronization | Yes | Flags, counters, lock-free state |
std::mutex | Exclusive access to a critical section | Yes | Protecting related or complex shared data |
const | Prevents modification through that name or reference | Not applicable | Read-only program values |
const volatile is possible:
const volatile std::uint32_t* statusRegister;
It means your code must not write through this pointer (const), but the underlying value may change externally and reads must remain observable (volatile). A read-only hardware status register is a common example.
Cheat Sheet
- Write
volatile int value;to qualify an object whose value may change outside ordinary C++ code. - A volatile read or write is an observable access; the compiler cannot freely eliminate or merge those accesses.
- Main C++ use: memory-mapped I/O and hardware registers.
volatileis not a replacement forstd::atomicorstd::mutex.volatiledoes not make reads, writes, or++atomic.const volatile Tis useful for externally updated, read-only registers.- Keep volatile operations inside low-level driver or platform code when possible.
- Use
std::atomicfor a cross-thread flag; use a mutex for shared multi-value state.
FAQ
Does volatile make a variable thread-safe in C++?
No. It does not prevent data races or provide synchronization. Use std::atomic or std::mutex.
Does volatile stop the compiler from optimizing code?
Not entirely. It specifically restricts optimization of accesses to the volatile object. The compiler may still optimize unrelated code.
When should I use volatile in C++?
Use it mainly for memory whose value is changed by hardware or another environment outside the normal C++ execution flow, such as a device register.
Is volatile the same in C++, Java, and C#?
No. The languages define different memory and threading semantics. Do not transfer Java or C# volatile advice directly to C++.
Should I use volatile for a boolean stop flag shared by threads?
No. Use std::atomic<bool> or a synchronization primitive such as a condition variable, depending on the design.
Can I combine const and volatile?
Yes. const volatile describes memory your code cannot write but which may change externally.
Why is volatile uncommon in everyday C++ applications?
Most applications communicate with external events through libraries, operating-system abstractions, atomics, mutexes, or message queues rather than direct hardware memory.
Mini Project
Description
Create a small simulated device-register wrapper. The project demonstrates how volatile reads and writes are kept inside a focused class instead of being spread through the program. The simulation uses ordinary memory so it can run as a normal C++ program; real hardware would supply the register address.
Goal
Build a Device class that reads a status register and writes a command register through volatile-qualified members.
Requirements
Define a register structure with volatile status and command fields.
Create a Device class that receives a pointer to those registers.
Add an isReady() method that checks bit 0 of the status register.
Add a start() method that writes 1 to the command register only when the device is ready.
Demonstrate both the not-ready and ready cases in main().
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.