What “volatile” does in C (and C++)

Paul J. Lucas - Sep 9 '21 - - Dev Community

Introduction

Occasionally, I come across either a use or explanation of the volatile keyword in C (or C++). Most often, that use or explanation is simply wrong. So here, hopefully once and for all, is what volatile in C (and C++) actually (and only) does.

Legitimate Uses of volatile

There are only three legitimate uses of volatile in C (and C++):

  1. To tell the compiler that objects qualified with volatile may either be modified or cause side effects in ways unknown to the compiler; therefore, not to optimize away accesses to such objects nor reorder their accesses with respect to other operations that have visible side effects.

  2. For use in a signal-handler.

  3. For use with setjmp().

That’s it. Any other use of volatile is innocuous, inefficient, or simply wrong.

Use 1: Optimization Suppression

The C11 standard (§ 6.7.3 ¶ 7) has the following to say about the first use of volatile:

An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects.

This typically means memory-mapped I/O, that is particular memory addresses are mapped either to specialized hardware or functions, so that reading from such an address actually reads a current value from such hardware (say, some kind of sensor) or writing to such an address actually causes a change in such hardware (say, sends a value over an old-fashioned modem).

For example, consider this code that sends all the characters of a C string over a modem that is mapped to the address 0x1000:

char *const MODEM = (char*)0x1000;    // memory-mapped address

void modem_send_s( char const *s ) {
    while ( *s )
        *MODEM = *s++;
}
Enter fullscreen mode Exit fullscreen mode

Writing a character to *MODEM sends that character. The problem is that a compiler might be clever and optimize that code to be:

void modem_send_s( char const *s ) {  // compiler optimized code
    while ( *s++ )
        ;
    *MODEM = s[-1];
}
Enter fullscreen mode Exit fullscreen mode

Under ordinary circumstances, it would be within its rights to do so (because if MODEM were a pointer to ordinary memory, only the last write matters). But in the memory-mapped I/O case, it would be wrong (because the compiler doesn’t know that address 0x1000 is special).

The C11 standard continues:

Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine....

That means no reads from nor writes to such an object will be optimized away nor instructions reordered by the compiler even if it normally would do so.

The way to tell the compiler not to optimize accesses to an object is by qualifying it with volatile:

char volatile *const MODEM = (char*)0x1000;
Enter fullscreen mode Exit fullscreen mode

(Read from right-to-left: MODEM is a constant pointer to a volatile char; or use cdecl. Just as with const, volatile can be written to the right or “east” of the base type.)

Use 2: Signal Handling

The C11 standard (§ 7.14.1.1 ¶ 5) has the following to say about the second use of volatile:

If the signal occurs ..., the behavior is undefined if the signal handler refers to any object with static or thread storage duration that is not a lock-free atomic object other than by assigning a value to an object declared as volatile sig_atomic_t ....

This means that if you want to refer to any object outside of the signal handler function, the type of that object (other than lock-free atomics) must be volatile sig_atomic_t. For example:

sig_atomic_t volatile last_sig_val;

void signal_handler( int sig_val ) {
    // ...
    last_sig_val = sig_val;
}
Enter fullscreen mode Exit fullscreen mode

Use 3: setjmp()

The C11 standard (§ 7.13.2.1 ¶ 3) has the following to say about the third use of volatile:

... the values of objects of automatic storage duration that are local to the function containing the invocation of the ... setjmp macro that do not have volatile-qualified type and have been changed between the setjmp invocation and longjmp call are indeterminate.

This means that if you modify a local variable between the time setjmp() is called and longjmp() returns, that variable must be declared volatile. For example:

void f() {
    volatile int count = 0;
    if ( setjmp( jmp_buf ) != 0 )
        g( ++count );
    // ...
}
Enter fullscreen mode Exit fullscreen mode

That’s it: those are the only three legitimate uses of volatile in C (and C++).

For more about setjmp(), see here.

A Digression on “Atomic”

Since the word “atomic” came up, it should be explained because it can have multiple meanings:

  1. A value operation completes with no possible intervening operation by another thread, e.g., writing single-byte values on any CPU, or 16- or 32-bit values on a 32-bit CPU, or 64-bit values on a 64-bit CPU.
  2. An updated value is visible to other CPUs.
  3. Multiple value operations complete with no possible intervening operation by another thread, e.g., updating A and B together (“transactional”).

“Atomic” always means #1; it usually also means #2; or all three. In the case of sig_atomic_t, however, it means only #1.

volatile in Other Languages

I’ve been consistently mentioning volatile as it pertains to C (and C++) to emphasize that this explanation of volatile pertains only to C (and C++) because the volatile keyword appears in other languages, for example Java and C#.

In those languages, volatile provides stronger guarantees regarding operations and therefore can be used for limited forms of thread-safety. And this can sometimes cause confusion as to what volatile means in C (and C++).

Wrong Uses of volatile

In C (and C++), volatile:

  • Is not a synonym for either _Atomic (in C) or std::atomic<T> (in C++).
  • Does not use memory barriers.
  • Therefore, does not guarantee thread-safety.
  • Limits only what optimizations the compiler may do.
  • Does not limit what the hardware can do.

In particular, the hardware is still free to do things like memory caching, instruction parallelization, and speculative execution.

Conclusion

volatile in C (and C++) is used only for memory-mapped I/O, signal handlers, or with setjmp(). Since those uses are rarely needed, it’s entirely possible to go years or even one’s entire career without ever using volatile in a C (or C++) program.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .