Why an implicit conversion from T* to const T* succeeds but from T** to const T** fails in C
Let T
be an (derived) object type.
Then consider the following example
T x;
T *y;
T const *p = &x; // ok
T const **pp = &y; // error
The type of the left-hand side of the first assignment is T const *
and of the right-hand side T*
.
The type T const *
is more restrictive than the type T*
in the sense that modifications to the pointed object are not allowed through this type.
Intuitively such an assignment should be fine and in the following we will see that this is indeed fine according to the C11 standard.
The second assignment is somewhat similar with the difference that a further level of indirection is added, i.e., a pointer to a pointer to a non-modifiable object. Though, this is a non-valid assignment according to the C11 standard. At first this might seem surprising, however, in the following we will see that such a restriction makes sense.
A simple assignment, i.e., a non-compound assignment, is defined in C11 section 6.5.16.1 where we have:
C11 § 6.5.16.1 Simple assignment ¶ 1
C11 § 6.3.2.1 Lvalues, arrays, and function designators ¶ 2
Two very subtle statements are made in these paragraphs. First, in order to decide type compatibility the qualifiers of the types pointed to are not taken into account, i.e., type compatibility is checked modulo outermost qualifiers. Second, the left-hand sides type may have additional qualifiers as long as it has all qualifiers of the right-hand sides type.
For the first assignment we have that the type of the left hand-side is T const *
and of the right-hand side the type is T*
.
Hence, the types pointed to are T const
and T
.
Since any qualifier of the type pointed to is not considered for the compatibility check, we have that the assignment is well-typed as T
is trivially compatible with itself.
For the second assignment we have that the type of the left hand-side is T const **
and of the right-hand side the types is T**
.
Hence, the types pointed to are T const *
and T*
which are not qualified, respectively.
Since T const *
and T*
are not compatible—T const
and T
are distinct types—the assignment is not well-typed.
Here we have again a subtle distinction.
q
the type T * q
is qualified whereas T q *
is not qualified.
The wording might be misleading, if you read it for the first time.
Consider a statement like “any qualifier of the type T
” and T
equals T' volatile * const
then the C11 committee intended only to consider the outermost qualifier of a type which is in this case only const
.
Although strictly speaking the qualifier volatile
is part of a subtype of T
and therefore you could argue that it is also a qualifier of T
itself.
However, this is not what the C11 committee intended.
Why is the second assignment not allowed?
From the arguments above we conclude that the second assignment is ill-typed and therefore not valid C11 code. Still the question remains why shouldn’t it be allowed? Is there a technical reason why not?
The short answer is that such constructs could lead to a type correct program where a constant location could be modified.
Consider the following code fragment
char *p;
char const **pp = &p; // constraint violation
*pp = "constant string";
*p = 'X';
If the first assignment would be valid, then we could create two pointers to a non-modifiable object where one pointer points to a non-const
-qualified type.
Therefore, we could modify in a type correct program an object which is supposed to be constant.
If you run the code fragment from above, it is quite likely that you will run into a segmentation fault.
You are still not convinced that this could lead to serious problems?
Then consider the following example where the function foo
is defined in a different compilation unit, e.g., a library, and you want to make use of it in function bar
.
void foo(char const **x) {
*x = "constant string";
}
void bar(void) {
char *p;
foo(&p); // constraint violation
*p = 'X';
}
The code looks innocent, though, contains undefined behavior because a string literal is modified as in the previous example.
C++ to the rescue
In C++ we can add a further const
-qualifier such that the pointer-assignment is type correct.
T *p;
T const * const *pp = &p; // ok
*pp = "constant string"; // compile-time error
Via the type of pointer pp
it is ensured that we cannot modify the pointer which it points to, i.e., a compile-time error is raised in the example.
Note that adding a further const
-qualifier in C, as done in the C++ example, does not help since the qualifier is ignored during type checking (remember the type of the left operand is the same as after lvalue conversion).