In C and C++ (indeed, most computer languages), there are keywords that mean very specific things to the compiler, and of course the programmer — but the two meanings are not necessarily identical!
This page describes common C/C++ keywords that have explicit meanings, but the (programmer’s) common view of those keywords may be subtly different. Fully understanding how the compiler treats the keywords may prevent errors in your code.
Contents
const
This is an easy one! const
means that the value never changes, right?
Actually, no. When applied to a variable, it means that you can never change the variable: your code is not allowed to assign a value to it. Something, somewhere else, may very well change the value.
Example
There are numerous circumstances where const
is used on a changeable object. The most common is when it is passed as const
. In that scenario, the “variable” is assumed to hold the same value for the duration of the call (which can be a poor assumption). But the important thing is that the compiler enforces that the code in the function cannot write to the object.
Actuality
In a simple program, on a simple system, it is (probably) true that a variable defined as const
never changes. However, in more complex systems a value might change “underneath” the code. For instance:
- An interrupt could change a value.
A careful programmer might declare a variable asconst
to prevent normal code that can access it from confusing an interrupt — but the interrupt could definitely change the value. - Another thread could change the value.
Similar to the interrupt case above, a careful programmer might declare read-only access to a variable that another function running in another thread will change.
More uses
And in C++, there are other uses of const
. One example is declaring that a member function doesn’t modify the class, so can be safely called on a const
object:
class String {
public: // Functions
String();
String(const char *string);
inline unsigned Length() const;
private: // Variables
const char *string;
unsigned length;
}; // String
Length()
promises not to modify the class, so can be called on a const String
. Any attempt by the code to modify a member variable will be flagged by the compiler as an error — unless that member is mutable
. So a const
function can still modify (specific) members of a class
: it’s not as const
as you thought it was!
Takeaway
const
doesn’t mean the value won’t change; it merely means that you can’t change it.
volatile
This is an esoteric keyword that many C/C++ programmers don’t know about — but when they learn about it, they think it does way more than it actually does!
Example
Here is some code that, on the face of it, looks ridiculous:
char key;
char WaitForKey() {
char temp;
do {
temp = key;
} while (temp == '\0');
key = '\0';
return temp;
} // WaitForKey()
This code keeps accessing the key
variable as long as it’s 0
. The only problem is, on the face of it, key
will always be 0
, or not. This is either an infinite loop, or a useless one!
What isn’t obvious is that key
can be modified by a keyboard interrupt. When a key on the keyboard is pressed, the interrupt will read the keypress and modify the key
variable. That will change temp
, and the function will exit with the value of the pressed key.
Intended usage
Perfect, right? Sorry, no. The programmer is not the only thing that can be confused by the above code. The compiler is allowed to use the same uninformed logic, and test key
once then decide from that test what to do from there: loop forever or not loop at all.
key
is declared as a normal variable, and so the compiler can make the assumption that, unless that code modifies the variable, the variable’s value won’t change. Since that’s not true (the interrupt can also modify it), the compiler needs to be told:
volatile char key;
Declaring a variable as volatile
does two things:
- It forces the compiler to explicitly re-read the variable every time it is used, rather than “deducing” its value from previous reads;
- It forces the compiler to explicitly write back any change to the variable as quickly as possible, rather than holding off in case it’s never changed again anyway.
Assumptions
When a programmer reads the above features of volatile
, they often think “Hey! That’s basically what a shared variable can suffer from. If I declare my variable as volatile
, then multiple threads can access the variable simultaneously and not cause contention!”
Only, no. The main problem is that while multiple (software) threads wouldn’t get confused with a volatile
variable, nobody told a multi-processor (hardware) architecture. Each processor (probably) has its own cache, and the code needs to explicitly tell the processor to update the cache — volatile
isn’t defined to do that.
Actually, it might work: for a “strong” memory model architecture like Intel/AMD’s x86/x64 architecture, cache coherency will work in the programmer’s favour. Unfortunately, ARM (for example) only has a weak memory model. You cannot rely on
volatile
to maintain inter-thread coherency — that’s not what it means!
Takeaway
All that volatile
means is that the compiler shouldn’t “remember” the contents of a variable for optimisation purposes — and that it should write any new value back as quickly as possible. There is no extra meaning with respect to multi-threading coherency: you need an explicit construct to do that, such as C++’s std::atomic<>
.