Language[lang]
volatile keyword in C++: what it does and does not do
- Section
- Language [lang]
- Standard
- C++17
- Compilers
- MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
- Published
- Length
- 8 min read
Abstract
The volatile keyword in C++ tells the compiler that a variable can change outside the normal flow of the code, so every read and write of it in the source is carried out. Use it for memory-mapped hardware registers and for a volatile std::sig_atomic_t flag set by a signal handler. It does not make a variable thread-safe: share a value between threads with std::atomic, or several with a mutex.

The volatile keyword in C++ tells the compiler that a variable can change outside the normal flow of your code, for example by a hardware device or a signal handler. Every read and write of it in your code then really happens: the compiler may not skip or merge them. It does not make a variable thread-safe; std::atomic does.
A stop flag set as if by Ctrl+C, in a whole program:
#include <csignal>
#include <iostream>
volatile std::sig_atomic_t stop_requested = 0;
extern "C" void on_interrupt(int) {
stop_requested = 1; // the only thing the handler does
}
int main() {
std::signal(SIGINT, on_interrupt);
int polls = 0;
while (!stop_requested) { // read again on every pass
++polls;
if (polls == 3) {
std::raise(SIGINT); // as if Ctrl+C were pressed
}
}
std::cout << "stopped after " << polls << " polls\n";
}— end example]
It printed stopped after 3 polls. std::raise runs the handler before it returns; a real Ctrl+C comes at any moment, and because stop_requested is volatile, the loop reads it again on every pass.
The samples are standard C++17, built with MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2 and Clang 23.1 (libc++), and every program printed the same on all four. Since When lists the compiler releases for each standard.
What does volatile do in C++?
It makes every access count. Each read and write of a volatile variable in the source is carried out, and within one thread the compiler keeps them in source order, though ordinary reads and writes may still move around them. In cppreference’s words, every access “is treated as a visible side-effect for the purposes of optimization” (cv type qualifiers). For an ordinary variable the compiler may reuse a value it has already read, or drop a write nothing reads, since the code it sees is all that can change it.

The standard’s reason is that a volatile object’s value may “change by means undetectable by an implementation”, the implementation being the compiler and its library ([dcl.type.cv]).
Write volatile beside the type, as you would const, and read a pointer declaration the same way (pointers in C++): before the * it applies to the int, after it to the pointer.
volatile int x = 10; // every read and write of x happens
int copy = x; // one read; copy is a plain int
int status = 7;
volatile int* p = &status; // pointer to volatile int
int* volatile q = &status; // volatile pointer to int— end example]
Printing copy, *p and *q gave 10 7 7. The other way round is a bug: reading or writing a volatile object through a pointer or reference without volatile, after a cast removed it, is undefined behavior, so the standard places no requirement on what the program does.
When do you need volatile?
In two cases: a hardware register mapped into memory, and a flag that a signal handler sets. The C++ Core Guidelines’ rule CP.200, which covers the first case, is “Use volatile only to talk to non-C++ memory”: objects “shared with ‘non-C++’ code or hardware that does not follow the C++ memory model”.
A memory-mapped register is a word of a device that sits at a fixed address: its value can change by itself, and reading or writing it can make the device act. Reach it through a pointer to volatile, so every write and every read your code contains is made at that address:
volatile std::uint32_t* const control = &fake_register;
*control = 0x1; // write: every write happens
std::uint32_t first = *control; // read it back
*control = 0x3; // write again
std::uint32_t second = *control; // read it back again— end example]
It printed 1 3. An ordinary variable stands in for the register, so the sample shows the syntax, not hardware; on a microcontroller control would hold the address from the chip’s datasheet, and the const after the * stops it pointing anywhere else.
The signal flag is the first program’s, declared volatile std:: from <csignal>. std::sig_atomic_t is “an integer type which can be accessed as an atomic entity even in the presence of asynchronous interrupts made by signals”, and volatile makes the loop read it again on every pass. Code that talks to neither a device nor a signal handler rarely needs volatile.
Is volatile thread-safe? volatile vs atomic
No. volatile does not make an operation atomic (indivisible) and does not synchronize threads, so two threads using one volatile variable, at least one of them writing, with no mutex or atomic between them, is a data race: undefined behavior. The title of the Core Guidelines’ CP.8 is “Don’t try to use volatile for synchronization”, and it adds: “It simply has nothing to do with concurrency.” In Java and C# the same word is a threading keyword; in C++ it is not.
For one value that threads share, use std::atomic:
std::atomic<int> polls{0};
void count_polls() {
for (int i = 0; i < 100000; ++i) {
++polls; // one indivisible read-modify-write
}
}
int main() {
std::thread a(count_polls);
std::thread b(count_polls);
a.join();
b.join();
std::cout << polls.load() << '\n';
}— end example]
All four printed 200000, with and without optimization, so no increment was lost. As a volatile int, the same counter would be a data race, which is undefined behavior, as std::atomic and the data races it prevents explains. For several values that must change together, use a mutex.
volatile vs const, and const volatile
const says your code will not write a value; volatile says something else may change it (const in C++). The two combine: a const volatile object “behaves as both a const object and as a volatile object” (cppreference), a value you only read that still changes by itself, such as a read-only status register:
const volatile std::uint32_t* status = &fake_status;
std::uint32_t before = *status; // read
device_finishes(); // the value changes, not by us
std::uint32_t after = *status; // read again— end example]
It printed 0 1: both reads happened, and the second saw the change. A write through status does not compile:
*status = 1; // error: const, so this code may not write it— end example]
All four rejected it; GCC said assignment of read-only location '* status'.
What did C++20 deprecate about volatile?
Not the keyword, but some uses of it. Since C++20, ++ and -- on a volatile variable and volatile function parameters and return types are deprecated (a parameter that points to volatile, such as volatile std::, is not): they still compile, and compilers may warn. Write the read and the write out instead, as v = v + 1; does:
int scaled(volatile int level) { // volatile parameter
return level * 2;
}
volatile int read_level() { // volatile return type
return 3;
}
int main() {
volatile int v = 0;
v++;
++v;
--v;
v += 1;
v |= 1;
v = v + 1; // the read and the write, spelled out
// ...
}— end example]
Under -std=c++20 and -std=c++23, GCC 16.2 and both Clangs reported v++, ++v, --v, the parameter and the return type as deprecated. GCC said:
warning: 'volatile'-qualified parameter is deprecated [-Wvolatile]
warning: 'volatile'-qualified return type is deprecated [-Wvolatile]
warning: '++' expression of 'volatile'-qualified type is deprecated [-Wvolatile]— end example]
Under C++17 none of them called these deprecated. MSVC said nothing at /W4: its warnings for these are off by default, and /Wall shows them.
v += 1; and v |= 1; drew no warning anywhere: compound assignment is not on cppreference’s current list, which adds two cases the samples do not cover: using the result of an assignment to a volatile, and a volatile structured binding.
Does MSVC treat volatile differently?
On x86 and x64, by default, yes. Microsoft’s compiler has two settings (/volatile): /volatile:iso “selects strict volatile semantics as defined by the ISO-standard C++ language”, and is the default for ARM targets other than ARM64EC. /volatile:ms, the default for x86, x64 and ARM64EC, adds that “acquire/release semantics are guaranteed on volatile accesses”: an ordering between threads that standard C++ does not promise.
Microsoft’s own advice: “We strongly recommend that you use /volatile:iso along with explicit synchronization primitives and compiler intrinsics when you are dealing with memory that is shared across threads.” Code that relies on /volatile:ms is not portable: in standard C++, and under /volatile:iso, it has a data race.
volatile, atomic or mutex: which do you use?
| You have | Use |
|---|---|
| a hardware register | a pointer to volatile |
| a read-only status register | a pointer to const volatile |
| a flag set by a signal handler | volatile std:: |
| one value shared between threads | std::atomic |
| several values that change together | a std::mutex |
| a value that never changes | const or constexpr |
The Language section has the other answers on the core language.
Questions
Can a volatile bool be a stop flag between threads?
No. A volatile variable that two threads use with no mutex or atomic between them, one of them writing, is still a data race, which is undefined behavior. A flag that one thread sets and another polls should be a std::atomic<bool>.
Is volatile deprecated in C++?
No, the keyword is not, but since C++20 some uses of it are: ++ and -- on a volatile variable, volatile function parameters and return types, using the result of an assignment to a volatile, and volatile structured bindings. Compound assignment such as v += 1 is not on that list. Write v = v + 1 in place of v++.
Does volatile slow a program down?
In standard C++ its effect is on the accesses to that variable: each read and write written in the source is carried out, where the compiler could remove or merge those of an ordinary variable. Any cost comes from those accesses, so it depends on how often the code makes them. MSVC’s default, /volatile:ms, also gives each access acquire/release ordering, which limits how other memory accesses can move around it.
What is a volatile member function?
A member function with volatile after its parameter list, as in std::uint32_t read_status() volatile, can be called on a volatile object, and inside it this points to a volatile object. A member function without volatile cannot: MSVC, GCC and Clang all rejected the call, Clang’s message ending “but function is not marked volatile”.
Is volatile in C++ the same as in Java?
No. In Java a volatile field is for threads: the Java Memory Model ensures that all threads see a consistent value for it. In standard C++ volatile has nothing to do with threads, and the C++ Core Guidelines say to use atomic types where you might have used volatile in another language.
Sources
- cv (const and volatile) type qualifierscppreference.com
- [dcl.type.cv] The cv-qualifiersC++ standard working draft
- std::sig_atomic_tcppreference.com
- CP.8: Don’t try to use volatile for synchronizationC++ Core Guidelines
- CP.200: Use volatile only to talk to non-C++ memoryC++ Core Guidelines
- /volatile (volatile Keyword Interpretation)Microsoft Learn
- Java Language Specification, 8.3.1.4 volatile FieldsOracle
- volatile (C# reference)Microsoft Learn