“A teacher is never a giver of truth; he is a guide, a pointer to the truth that each student must find for himself.”
— Bruce Lee
In part I of this series, I explained what pointers are in general, how they are similar to arrays, and — more importantly — where, when, and why they are different to arrays. Today, I’ll shed some light on the so-called ‘cv qualifiers’ which are frequently encountered in pointer contexts.
CV-QUALIFIER BASICS
CV-qualifiers allow you to supplement a type declaration with the keywords ‘const’ or ‘volatile’ in order to give a type (or rather an object of a certain type) special treatment. Take ‘const’, for instance:
|
const double PI = 3.1415927; PI = 1.23; // Error, PI is constant. PI += 1; // dito. |
‘const’ is a guarantee that a value isn’t (inadvertently) changed by a developer. On top of that, it gives the compiler some leeway to perform certain optimizations, like placing ‘const’ objects in ROM/non-volatile memory instead of (expensive) RAM, or even not storing the object at all and instead ‘inline’ the literal value whenever it’s needed.
‘volatile’, on the other hand, prevents optimizations. It’s a hint to the compiler that the value of an object can change in ways not known by the compiler and thus the value must never be cached in a processor register (or inlined) but instead always loaded from memory. Apart from this ‘don’t optimize’ behavior, there’s little that ‘volatile’ guarantees. In particular — and contrary to common belief — it’s no cure for typical race condition problems — It’s mostly used in signal handlers and to access memory-mapped hardware devices.
Even if it sounds silly at first, it’s possible to combine ‘const’ and ‘volatile’. The following code declares a constant that shall not be inlined/optimized:
|
const volatile int MAX_SENSORS = 4; ... for (int i = 0; i < MAX_SENSORS; ++i) { // Always load MAX_SENSORS // value from memory. sum += sensors[i].value; } |
Using both ‘const’ and ‘volatile’ together makes sense when you want to ensure that developers can’t change the value of a constant and at the same time retain the possibility to update the value through some other means, later. In such a setting, you would place ‘MAX_SENSORS’ in a dedicated non-volatile memory section (ie. flash or EEPROM) that is independent of the code, eg. a section that only hosts configuration values*. By combining ‘const’ and ‘volatile’ you ensure that the latest configuration values are used and that these configuration values cannot be altered by the programmer (ie. from within the software).
To sum it up, ‘const’ means “not modifiable by the programmer” whereas ‘volatile’ denotes “modifiable in unforeseeable ways”.
CV-QUALIFIERS COMBINED WITH POINTERS
Like I stated in the intro, cv-qualifiers often appear in pointer declarations. However, this poses a problem because we have to differentiate between cv-qualifying the pointer and cv-qualifying the pointed-to object. There are “pointers to ‘const'” and “‘const’ pointers”, two terms that are often confused. Here’s code involving a pointer to a constant value:
|
const int MAX_RATE = 200; const int MIN_RATE = 10; int default_rate = 42; const int* rate; rate = &MAX_RATE; // Point to memory containing MAX_RATE. rate = &MIN_RATE; // Now point to memory containing MIN_RATE. *rate = 1000; // Error: pointer-to-const cannot modify // pointed-to object. rate = &default_rate // Point to non-const value. *rate = 1000; // Error: pointer-to-const cannot modify // pointed-to object. |
Since the pointer is declared as pointing to ‘const’, no changes through this pointer are possible, even if it points to a mutable object in reality.
Constant pointers, on the other hand, behave differently. Have a look at this example:
|
int default_rate = 42; // Non-const value. int current_rate = 19; // dito. int* const p; // Error: const pointers must be // initialized. int* const p = ¤t_rate; // Fine, point to a non-const value. *p = 50; // Indirectly update current rate. p = &default_rate // Error: const pointers can't be // bound to another object. ++p; // dito. |
The takeaway is this: if the ‘const’ keyword appears to the left of the ‘*’, the pointed-to value is ‘const’ and hence we are dealing with a pointer to ‘const’; if the ‘const’ keyword is to the right of the ‘*’, the pointer itself is ‘const’. Of course, it’s possible to have the ‘const’ qualifier on both sides at the same time:
|
const int * const rate = &MAX_RATE; *rate = 42; // Error: pointer to const can't // modify value. ++rate; // Error: const pointer can't // point elsewhere. |
The same goes for multi-level pointers:
Here, ‘v’ is a regular (non-‘const’) pointer to ‘const’ pointer to a pointer to a ‘const’ integer.
Yuck! Sometimes, I really wish the inventors of C had used ‘<-‘ instead of ‘*’ for pointer declarations — the resulting code would have been easier on the eyes! Consider:
versus
|
int <- p; // say: "p is a POINTER TO int" |
So
would read from right to left as “v is a POINTER TO const POINTER TO const int”. Life would be some much simpler… but let’s face reality and stop day-dreaming!
Everything I said about ‘const’ equally applies to pointers to ‘volatile’ and ‘volatile’ pointers: pointers to ‘volatile’ ensure that the pointed-to value is always loaded from memory whenever a pointer is dereferenced; with ‘volatile’ pointers, the pointer itself is always loaded from memory (and never kept in registers).
Things really get complicated when there is a free mix of ‘volatile’ and ‘const’ keywords with pointers involving more than two levels of indirection:
|
volatile int * const volatile * volatile * p; |
Let’s better not go there! If you are in multi-level pointer trouble, remember that there’s a little tool called ‘cdecl‘ which I showcased in the previous episode. But now let’s move on to the topic of how and when cv-qualified pointers can be assigned to each other.
ASSIGNMENT COMPATIBILITY I
Pointers are assignable if the pointer on the left hand side of the ‘=’ sign is not more capable than the pointer on the right hand side. In other words: you can assign a less constrained pointer to a more constrained pointer, but not vice versa. If you could, the promise made by the constrained pointer would be broken:
|
const int* pc; int* p; pc = p; // OK, since 'p' is a read/write pointer and // 'pc' is a read-only pointer. p = pc; // Error: 'pc' is more constrained than 'p'. |
If the previous statement was legal, a programmer could suddenly get write access to a read-only variable:
|
const int VALUE = 42; const int* pc = &VALUE; // Equal restrictiveness on both // sides (ie. const). *pc = 43; // Error: no write access. int* p = pc; // Let's pretend this was legal... *p = 43; // const value updated! |
Again, the same restrictions hold for pointers to ‘volatile’. In general, pointers to cv-qualified objects are more constrained than their non-qualified counterparts and hence may not appear on the right hand side of an assignment expression. By the same token, this is not legal:
|
const volatile int* pcv; const* pc; pc = pcv; // Error: right hand side is more constrained... pcv = pc // OK. |
ASSIGNMENT COMPATIBILITY II
The rule which requires that the right hand side must not be more constrained than the left hand side might lead you to the conclusion that the following code is perfectly kosher:
|
int value = 100; int* p = &value; int** pp = &p; const int** ppc = pp; // Error: incompatible assignment. |
However, it’s not, and for good reason, as I will explain shortly. But it’s far from obvious and it’s a conundrum to most — even seasoned — C developers. Why is it possible to assign a pointer to non-const to a pointer to ‘const’:
|
const int *pc; int* p; pc = p; // OK. |
but not a pointer to a pointer to non-const to a pointer to a pointer to ‘const’?
|
const int** ppc; int** pp; ppc = pp; // Error. |
Here is why. Imagine this example:
|
const int VALUE = 42; int* p; const int** ppc; ppc = &p; // Error, but let's pretend this was legal. |
Graphically, our situation is this. ‘ppc’ points to ‘p’ which in turn points to some random memory location, as it hasn’t been initialized yet:
|
VALUE 0x00B00010: 00 00 00 2A // 42 : : p 0x00004220: ?? ?? ?? ?? // Points to random location ppc 0x00004224: 00 00 42 20 // Points to 'p' |
Now, when we dereference ‘ppc’ one time, we get to our pointer ‘p’. Let’s point it to ‘VALUE’:
It shouldn’t surprise you that this assignment is valid: the right hand side (pointer to const int) is not less constrained than the left hand side (also pointer to const int). The resulting picture is this:
|
VALUE 0x00B00010: 00 00 00 2A // 42 : : p 0x00004220: 00 B0 00 10 // Now points to 'VALUE' ppc 0x00004224: 00 00 42 20 // Points to 'p' |
Everything looks safe. If we attempt to update ‘VALUE’, we won’t succeed:
|
**ppc = 666; // Error: can't update through pointer to 'const'. |
But we are far from safe. Remember that we also (indirectly) updated ‘p’ which was declared as pointing to a non-const int and ‘p’ was declared as pointing to non-const? The compiler would happily accept the following assignment:
which leads to undefined behavior, as the C language standard calls it.
This example should have convinced you that it’s a good thing that the compiler rejects the assignment from ‘int**’ to ‘const int**’: it would open-up a backdoor for granting write access to more constrained objects. Finding the corresponding words in the C language standard is not so easy, however and requires some digging. If you feel “qualified” enough (sorry for the pun), look at chapter “6.5.16.1 Simple assignment”, which states the rules of objects assignability. You probably also need to have a look at “6.7.5.1 Pointer declarators” which details pointer type compatibility as well as “6.7.3 Type qualifiers” which specifies compatibility of qualified types. Putting this all into a cohesive picture is left as an exercise to the diligent reader.
________________________________
*) Separating code from configuration values is generally a good idea in embedded context as it allows you to replace either of them independently.↩