13.6 Volatile Objects

The keyword volatile is often misunderstood in portable code. Its use inhibits some memory-access optimizations, but programmers often wish that it had a different meaning than it actually does.

volatile was designed for code that accesses special objects like memory-mapped device registers whose contents spontaneously change. Such code is inherently low-level, and it is difficult to specify portably what volatile means in these cases. The C standard says, “What constitutes an access to an object that has volatile-qualified type is implementation-defined,” so in theory each implementation is supposed to fill in the gap by documenting what volatile means for that implementation. In practice, though, this documentation is usually absent or incomplete.

One area of confusion is the distinction between objects defined with volatile types, and volatile lvalues. From the C standard’s point of view, an object defined with a volatile type has externally visible behavior. You can think of such objects as having little oscilloscope probes attached to them, so that the user can observe some properties of accesses to them, just as the user can observe data written to output files. However, accesses via volatile lvalues to ordinary objects are merely side effects (i.e., changes to the state of the execution environment), and the implementation is not required to document their visibility any further. For example:

/* Declare and access a volatile object.
   Accesses to X are "visible" to users.  */
static int volatile x;
x = 1;

/* Access two ordinary objects via a volatile lvalue.
   Although each read and write is a side effect,
   the accesses are not directly "visible" to users.  */
int y = 0;
int *z = malloc (sizeof *z);
*z = 7;
int volatile *p;
p = &y;
*p = *p + 1;
p = z;
*p = *p + 1;

Programmers often wish that volatile meant “Perform the memory access here and now, without merging several memory accesses, without changing the memory word size, and without reordering.” But the C standard does not require this. For objects defined with a volatile type, accesses must be done before the next sequence point; but otherwise merging, reordering, and word-size change is allowed.

Even when accessing objects defined with a volatile type, the C standard allows only extremely limited signal handlers: in C23 the behavior is undefined if a signal handler refers to any non-local object that is not a lock-free atomic object and that is not constexpr (other than by writing to a sig_atomic_t volatile object), or calls any standard library function other than from a small set that includes abort, _Exit, quick_exit, some <stdatomic.h> functions, and signal. Hence C compilers need not worry about a signal handler disturbing ordinary computation. POSIX allows some additional behavior in a portable signal handler, but is still quite restrictive. See When is a Volatile Object Accessed? in Using the GNU Compiler Collection (GCC), for some restrictions imposed by GCC. See Defining Signal Handlers in The GNU C Library, for some restrictions imposed by the GNU C library. Restrictions differ on other platforms.

If possible, it is best to use a signal handler that fits within the limits imposed by the C and POSIX standards.

If this is not practical, you can try the following rules of thumb. A signal handler should access only volatile lvalues, preferably lvalues that refer to objects defined with a volatile type, and should not assume that the accessed objects have an internally consistent state if they are larger than a machine word. Furthermore, installers should employ compilers and compiler options that are commonly used for building operating system kernels, because kernels often need more from volatile than the C Standard requires, and installers who compile an application in a similar environment can sometimes benefit from the extra constraints imposed by kernels on compilers. Admittedly we are hand-waving somewhat here, as there are few guarantees in this area; the rules of thumb may help to fix some bugs but there is a good chance that they will not fix them all.

For volatile, C++ has the same problems that C does. Multithreaded applications have even more problems with volatile, but they are beyond the scope of this section.

The bottom line is that using volatile typically hurts performance but should not hurt correctness. In some cases its use does help correctness, but these cases are often so poorly understood that all too often adding volatile to a data structure merely alleviates some symptoms of a bug while not fixing the bug in general.