Let’s consider following piece of code from the C++ standard:
int main() { char* pc; const char** pcc = &pc; }
Can you see the issue here? theoretically we are assigning the pointer to the pointer to the non const
to the pointer to pointer to the const char
. On first sight nothing dangerous happens here. After all we are assigning the pointer, through which we can change the value to one, through which it cannot be modified. But if we extend the example:
int main() { const char c = 'c'; char* pc; const char** pcc = &pc; *pcc = &c; //modifies a const object *pc = 'C'; }
Then it becomes clear, that this is dangerous, because pointer to non const pc
starts pointing to the const char
variable and later on we might try to modify the variable pointed by pc
– c
.
Fortunatelly, the example will not compile due to C++ standard conversion rules (conversion rules defined by the standard, that happen outside of the user’s control) and specifically Qualification conversion rule.
Qualification conversion rules were created, so that const correctness is preserved.
To understand Qualification conversion we need first understand what are simillar types and what is cv decomposition of type.
CV decomposition
Formally cv decomposition of type T is defined as follows according to the C++ standard
cv(0) P(0) cv(1) P(1) ⋯ cv(n−1) P(n−1) cv(n) U ,
,where:
- cv(k) is the const volatile qualifications of the P(k), cv(k) can be empty. cv(n) refer to the const volatile qualifications of the U type.
- P(k) is simply a pointer
- U is a type which we will get reference to after dereferencing all the pointers
To make it more intuitive, the cv(n) qualification is a qualification of the most inner type. Now going from the most inner to the most outer pointer we are writing down the cv qualifications of the pointers.
Let’s do the decomposition of the type given in the example, step by step:
- const char** ->count number of pointers to deduce n=2
- const char** -> deduce U = char
- const char ** -> check cv qualifiers of U cv(2) = const
- const char ** -> check type nearest to U – P(1) = * (pointer)
- const char ** -> check cv qualification of P(1) = empty
- const char ** ->check type nearest to P(1) – P(0) = * (pointer)
- const char ** ->check cv qualification of P(0) = empty
So the decomposition of the type can be summarized in the table below:
var | value |
n | 2 |
U | char |
cv(2) | const |
cv(1) | empty |
P(1) | * |
cv(0) | empty |
P(0) | * |
Let’s see some other examples of such decomposition.
Example 1
using T1 = const char * const **;
decomposition of T1 is following:
var | value |
n | 3 |
U | char |
cv(3) | const |
cv(2) | const |
P(2) | * |
cv(1) | empty |
P(1) | * |
cv(0) | empty |
P(0) | * |
Example 2
using T2 = const char ***;
decomposition of T2 is following:
var | value |
n | 3 |
U | char |
cv(3) | const |
cv(2) | empty |
P(2) | * |
cv(1) | empty |
P(1) | * |
cv(0) | empty |
P(0) | * |
Now if you understand what is cv decomposition of the types, then you are almost ready to get familiar with how qualification conversion work. There is just one tiny thing to explain:
Simillar types
Types are considered to be simillar if after cv decomposition for two types following are true:
- U (from the math equation) are deduced to be the same
- n (that denotes the number of “pointers” in the type) are deduced to be the same
- cv qualifications needs not to be the same
Intuitivelly, simillar types are the types, which are the same after removing all the constness and volatileness.
Qualification conversion rules
Qualification conversion have 3 steps. For type T1, that is to be converted into the type T2 following type properties are checked:
- Whether type T1 and Type T2 are simillar
- Check whether all cv qualifications are the same
- If there is a place where qualifications do not match, then check whether all qualifications before this place(down the decomposition table) except the first one have const qualifiers (first one can, but is not obliged to have const qualifier).
After performing those checks and the result of the check is OK, then conversion is allowed. Otherwise we will experience the compilation error, that might look like this:
error from the gcc
error: invalid conversion from ‘char**’ to ‘const char**
error from the clang
error: cannot initialize a variable of type ‘const char **’ with an rvalue of type ‘char **’
Make it compile again
So coming back to the example from the beginning:
int main() { char* pc; const char** pcc = &pc; }
What we should do to make it compile?
So let’s first find the reason of why this code sample will not compile or to rephrase it “why qualification conversion was not applied here”?
First, for both types, which in this cases are const char **
and char **
. The deduction of the Us would be the same and U would be deduced as a char. Also ns are deduced to be the same, since both types have the same amount of stars. Because of that we know, that types are simillar.
We are switching to the second point, and we are checking whether all cv qualifications are the same. They are not, since the first cv qualifications near U differ.
Since cv qualifications differ we want to check whether the remaining cv qualifications (except the last one) that are to be analyzed have const qualifiers. They do not.
Which leads to the compilation issue.
There are two ways we can fix it. Either we should make all cv qualifications match, or make all qualifications (except the last one) remaining after the one, that does not match to have a const qualifier. So it might either be:
int main() { char* pc; char** pcc = &pc; // types exactly the same }
or
int main() { char* pc; const char* const* /* optionally here also const*/ pcc = &pc; }
Summary
This is a short post, but I think it might help you understanding what the hell is wrong if you encounter this kind of an error, since at the first sight it’s counterintuitive on why compilation error happened.
I hope, that after reading this post you will be familiar with why the qualification conversion exists and why it would be unsafe in the example given at the beginning, to allow the cast of the pointers. Cheers!
Bibliography
- http://eel.is/c++draft/conv.qual (also code example is from here)
- https://stackoverflow.com/questions/54886467/ambiguity-in-qualification-conversion