24
$\begingroup$

C++ does not generate default copy/move constructors or assignment operators for any volatile struct or class type. That is, given these three declarations, all of the following statements are illegal:

class Foo {}; // or struct Foo {};
Foo a;
volatile Foo b;

// ALL of these are illegal:
a = b;
b = a;
Foo c = b;
Foo d = std::move(b);
volatile Foo e = b;
b = b; // a=a is useless but allowed. b=b is not permitted.

Furthermore, if you define constructors and operator=s to allow these, then all the default ones go away, and you have to explicitly default them. Defining all this is a massive amount of boilerplate even for a small PoD class:

class Bar {
  int m_a, m_b;
public:
  constexpr Bar(int a, int b) noexcept : m_a(a), m_b(b) {}
  // Everything below this line is the aforementioned boilerplate
  Bar(const Bar&) = default;
  Bar(Bar&&) = default;
  Bar& operator=(const Bar&) = default;
  Bar& operator=(Bar&&) = default;
  Bar(const volatile Bar& other) noexcept : m_a(other.m_a), m_b(other.m_b) {}
  Bar(volatile Bar&& other) noexcept : m_a(std::move(other.m_a)), m_b(std::move(other.m_b)) {}
  Bar& operator=(const volatile Bar& other) noexcept {
    m_a = other.m_a;
    m_b = other.m_b;
    return *this;
  }
  volatile Bar& operator=(const volatile Bar& other) volatile noexcept {
    m_a = other.m_a;
    m_b = other.m_b;
    return *this;
  }
  Bar& operator=(volatile Bar&& other) noexcept {
    m_a = std::move(other.m_a);
    m_b = std::move(other.m_b);
    return *this;
  }
  volatile Bar& operator=(volatile Bar&& other) volatile noexcept {
    m_a = std::move(other.m_a);
    m_b = std::move(other.m_b);
    return *this;
  }
};

Why does C++ not generate these constructors and assignment operators for volatile types? Were they just considered unnecessary, or is there some technical reason they shouldn't exist by default?

$\endgroup$
2
  • $\begingroup$ Often unnoticed: The C language guarantees that volatile objects that have been modified between setjmp() and longjmp() are not restored. Usually longjmp() will restore all registers and volatile keeps things out of registers so this will happen by coincidence, but the language actually gives a guarantee. $\endgroup$
    – gnasher729
    Dec 3, 2023 at 12:51
  • $\begingroup$ related cppcon talk: Deprecating volatile - JF Bastien - CppCon 2019 $\endgroup$
    – starball
    Dec 16, 2023 at 5:41

3 Answers 3

52
$\begingroup$

What volatile actually means

The meaning of volatile in C and C++ is often poorly understood. The main use case for volatile is low-level programming on embedded systems that utilize memory-mapped I/O. In these systems, portions of the address space are not mapped to ROM or RAM; instead, reads from or writes to these special addresses are arbitrarily interpreted as commands by other hardware attached to the bus. In these cases, the usual properties of memory do not apply:

  • When an address refers to memory, merely reading the value at that address has no side effects, so unused reads can be deleted by the compiler without changing program semantics. However, when an address refers to a memory-mapped I/O device, a “read” can actually modify the device’s state!

    As a very simple hypothetical example of this, an address might be mapped to a shift register. Each time the address is read, one value might be shifted out, so repeated reads will return successive values in the shift register. “Unused” reads still advance the register, so removing them changes the program’s semantics.

  • Similar to the above, when an address refers to memory, reading from it multiple times with no intervening writes or synchronizations can always be assumed to return the same value. As the shift register example above illustrates, this is not necessarily true.

  • When an address refers to memory, writing to an address multiple times with no intervening reads or synchronizations can always be safely coalesced to a single write (of the final value). If the address refers to a memory-mapped I/O device, each store can have side effects, so this is not safe in general.

  • When an address refers to RAM, a write followed immediately by a read of the same memory address can always be assumed to return the written value. If the address refers to a memory-mapped I/O device, reads and writes to the same address can be interpreted in completely different ways, so there is no requirement that this be true.

In other words, reads from or writes to an address mapped to an I/O device is really more like a kind of hardware procedure call than a traditional load or store from memory. This is incompatible with the standard memory semantics of the C and C++ abstract machines, which assume that memory addresses refer to real memory and therefore behave in the usual way.

volatile is used to instruct a C or C++ compiler that a variable contains an address that is not mapped to conventional memory. This suppresses most of the compiler’s memory-related optimizations: loads and stores through volatile references are never dropped, coalesced, duplicated, or reordered. In other words, loads and stores through volatile addresses are optimized in much the same way as a call to an unknown function.

volatile is not useful for multithreading

There is a widespread (false) belief that volatile is useful in multithreaded programs to ensure mutations are consistently visible across threads, but this is not the case. volatile only requires that the compiler emit a load or store instruction for each load or store in the source program, but this is not sufficient to ensure consistent visibility across threads on hardware that uses a weak memory model. On such systems, speculative execution can reorder non-conflicting loads and stores to conventional memory, so even if volatile is used, memory barriers are still necessary to ensure consistent ordering of mutations between threads.

(In theory, a C or C++ compiler could interpret volatile differently and automatically insert memory barriers to make it useful in this way. However, this is not guaranteed by the standard for either language, and GCC and Clang do not interpret it this way. By default, MSVC does interpret it this way on x86 and x86_64, but it does not on ARM, and it inhibits useful optimizations that more targeted synchronization constructs do not, so relying on this behavior is not recommended.)

volatile being a qualifier is somewhat strange

The fact that volatile is a qualifier like const is a somewhat strange decision because it is not really a property of the variable. Instead, volatile essentially changes the meaning of the * and -> operators when used on that variable. Instead of providing the volatile keyword, C could have simply provided volatile reads and writes as separate operations. For example, instead of writing

volatile unsigned char* io_addr = 0xdeadbeef;
*io_addr = 0x01;
return *io_addr;

one could write

mio_store(0xdeadbeef, 0x01);
return mio_load(0xdeadbeef);

which would arguably make it much clearer that these are not ordinary reads and writes but more like procedure calls. Indeed, this is precisely the approach that Rust takes: it provides the std::ptr::read_volatile and std::ptr::write_volatile functions instead of providing a special qualifier.

C’s decision to treat volatile as a qualifier makes more sense when considered from a historical perspective. C’s original purpose was essentially to be a fancy portable macro assembler, and C programs were originally written by assembly programmers who expected to be able to predict the assembly that C compilers would generate. From this point of view, it makes sense to want a guarantee that loads and stores to certain variables really produce loads and stores, and this naturally led to the volatile keyword. Modern C and C++ compilers diverged from this original philosophy decades ago, so the design of volatile has unfortunately outlived its original usefulness.

volatile objects and C++ classes

All of the above describes the meaning of volatile when used on references to primitive types, like char* or int*, but C++ substantially complicates the meaning of const and volatile qualifiers, also known as cv-qualifiers. When volatile is applied to a variable of composite type like a struct or class, then references to members through that variable are considered volatile as well. Additionally, const and volatile can also be used to qualify methods, and if a method is invoked through a cv-qualified object, the method must be appropriately cv-qualified.

Given the meaning of volatile, this restriction makes sense. Keep in mind that every volatile load and store should be treated like a side-effectful procedure call, so order very much matters! This substantially complicates the meaning of copy constructors and operator=, each of which perform several sequential reads and writes to the objects’ members. Changing the order in which these assignments are performed could very well result in completely different behavior due to different ordering of effects, so it is likely better that the programmer specify that order explicitly.

All that said, by this point you should hopefully be wondering whether a volatile-qualified composite object is terribly useful in the first place. Given that volatile is generally useful with memory-mapped I/O, the addresses targeted by volatile loads and stores are generally fixed and statically-known. It makes little sense to copy an object from a volatile reference because the resource identified by a memory-mapped I/O address is a piece of actual hardware that cannot simply be duplicated. In most cases, volatile should not be used at all, and when it is, it should be used on values of primitive type, not objects.

$\endgroup$
14
  • 3
    $\begingroup$ "volatile is not useful for multithreading" -- Although this caveat almost certainly isn't relevant to OP's question, I do like to qualify this with "not useful at the application level". It can sometimes have a place deep in the guts of the implementation of concurrency primitives. (And one can also use it correctly for sharing state with signal handlers, which are not exactly concurrency, but are not mmio either. However, now that atomics exist, one can and should use those instead.) $\endgroup$ Dec 1, 2023 at 3:42
  • 6
    $\begingroup$ @GlennWillen In a multithreaded context, it is truly never useful: even if atomics cannot be used (which is rare), making volatile useful would still require the use of memory barriers, but the ordering constraints imposed by atomic_thread_fence apply to non-volatile references (for better or for worse), so the volatile qualifier serves no purpose. I concede that it is theoretically useful for state used by signal handlers, though as you say, this seems more academic than practical. $\endgroup$
    – Alexis King
    Dec 1, 2023 at 7:59
  • 7
    $\begingroup$ "volatile is not useful for multithreading" – It probably doesn't help that in Java, a language with superficially C-like syntax, that is exactly what volatile is most useful for. So, when programmers come out of a "Java school" and later learn C or C++, they have this meaning engrained in their brains. $\endgroup$ Dec 1, 2023 at 12:16
  • 8
    $\begingroup$ Nitpick: A volatile struct can be useful to concretely define the layout of a MMIO region that has multiple related components to it such that you can refer to that region with a single variable or reuse the layout for multiple regions if you have multiple devices with the same layout. This can make code utilizing that MMIO region easier to read, because it groups all the bits together as one logical unit. I agree though that a volatile class in C++ is essentially never useful. $\endgroup$ Dec 1, 2023 at 12:24
  • 2
    $\begingroup$ Thanks for this explanation, especially about MSVC behavior on x86. I've seen the claim the volatile isn't useful for ensuring that writes are visible across threads many times but had never made the connection that this is a particular feature of the x86 (the only CPU with which I am at all familiar) rather than simply how volatile works. It may also not help that when the texts for C and C++ were first written multi-core systems really were not of concern to the typical programmer. Those texts basically said "volatile is a pessimization ensuring that all reads and writes actually occur". $\endgroup$ Dec 2, 2023 at 21:23
5
$\begingroup$

In a follow on comment, OP mentioned that his use case is on ISRs. Because an ISR is so tightly constrained on what it can do, and the nature of interrupts guarantees their memory ordering, the following is one of the valid uses of volatile in the context of this question:

volatile sig_atomic_t isrFlag{0};

void interrupt_handler() { isrFlag = 1; }

int main() {
    for (;;) {
       if (isrFlag) {
           // Do things
       }
    }
}

There is no need for an additional memory fence here. https://en.cppreference.com/w/c/program/sig_atomic_t

$\endgroup$
1
$\begingroup$

Although this was well explained in the previous answers, I like when you can demonstrate it with a real-life example.

I admit I never saw usage of the volatile qualifier, except in the embedded software world for primitive types, i.e., pointers to memory locations. And I don’t know what purpose it would serve in the C++ object world.

Compilers consider memory access "expensive" and try to avoid it whenever they can, by storing its content in CPU registers.

The purpose of volatile is just to tell compiler not to make any assumptions and optimizations regarding handling variables and memory access.

Basically, it must assume that the content of a memory location accessed by a pointer can change independently of our code, at any time (asynchronously by hardware, in the ISR).

Let’s see those examples. Here code reads some peripheral status register and checks when the content of a register changes to 0x02:

uint8_t * stat_reg = (uint8_t *) 0x08001234;
// Wait for register to become non-zero
do { ... } while (0 == *stat_reg)

The compiler doing optimization could do something like this. Having already read the register content from the peripheral register at 0x1234, the compiler figures there isn't any need reading from this memory location again, so the compiler places the content of memory in the CPU accumulator and checks the accumulator instead:

mov stat_reg, #0x08001234
mov a, @stat_reg
loop:
   ...
   bz loop

Needless to say why this assembly code would never work.

So volatile is used here to tell the compiler not to make any optimizations when accessing the address the pointer points to.

uint8_t volatile * stat_reg = (uint8_t volatile *) 0x08001234

Let’s try it with a real example with the help of GDB:

#include <stdio.h>

int func1(volatile int* i) {
    int a = 31;

    while (*i == 0)
        a++;

    return a;
}

volatile in reg = 0;

int main() {
    return reg + func1(&reg);
}

If we disassemble our snippet where variable reg represents our hardware register:

   0x000055555555512d <+4>:     mov    (%rdi),%eax
   0x000055555555512f <+6>:     test   %eax,%eax
   0x0000555555555131 <+8>:     jne    0x555555555142 <func1+25>
   0x0000555555555133 <+10>:    mov    $0x1f,%eax
   0x0000555555555138 <+15>:    add    $0x1,%eax
=> 0x000055555555513b <+18>:    mov    (%rdi),%edx
   0x000055555555513d <+20>:    test   %edx,%edx
   0x000055555555513f <+22>:    je     0x555555555138 <func1+15>

We see that in each iteration value of volatile register, (which address is in $rdi) will be loaded from memory, mov (%rdi),%edx, and tested, test %edx,%edx.

Now if we recompile it without the volatile qualifier, the compiler assumes that the value of reg never changes, and there isn't any need for reading from memory in each iteration, so the while loop condition is always true. We end with an infinite loop in optimized code:

   0x000055555555512d <+4>:     cmpl   $0x0,(%rdi)
   0x0000555555555130 <+7>:     jne    0x555555555134 <func1+11>
=> 0x0000555555555132 <+9>:     jmp    0x555555555132 <func1+9>
   0x0000555555555134 <+11>:    mov    $0x1f,%eax
$\endgroup$
7
  • 1
    $\begingroup$ I mentioned this in the comments on another answer, but I’m using the volatile objects to communicate between interrupt handlers and the main function. The interrupt handlers check certain sensors and then write the sensor data to a global variable, and unless that variable is volatile the main function will never see the sensor data. $\endgroup$
    – Bbrk24
    Dec 2, 2023 at 12:28
  • 3
    $\begingroup$ Welcome. I am confused as to why you posted a screenshot of the code as opposed to the text of the code. Besides that, this is a great first answer. $\endgroup$ Dec 2, 2023 at 18:08
  • $\begingroup$ Sorry, is there some other reason against posting screenshot, or just because you cannot copy example? $\endgroup$ Dec 2, 2023 at 18:25
  • 3
    $\begingroup$ @DrazenGrasovec Posting a screenshot requires more data for others to load and isn't as easy to read for visually impaired users. $\endgroup$
    – Bbrk24
    Dec 2, 2023 at 19:58
  • 3
    $\begingroup$ Accessibility is the main reason not to post text/code as images, as Bbrk mentioned. As well as the fact that some people can't access Imgur/have images disabled. $\endgroup$ Dec 2, 2023 at 22:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .