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
[…] (considering the type the left operand would have after lvalue conversion) both operands are pointers to qualified or unqualified versions of compatible types, and the type pointed to by the left has all the qualifiers of the type pointed to by the right […]
One of the following shall hold:112)
  • the left operand has atomic, qualified, or unqualified arithmetic type, and the right has arithmetic type;
  • the left operand has an atomic, qualified, or unqualified version of a structure or union type compatible with the type of the right;
  • the left operand has atomic, qualified, or unqualified pointer type, and (considering the type the left operand would have after lvalue conversion) both operands are pointers to qualified or unqualified versions of compatible types, and the type pointed to by the left has all the qualifiers of the type pointed to by the right;
  • the left operand has atomic, qualified, or unqualified pointer type, and (considering the type the left operand would have after lvalue conversion) one operand is a pointer to an object type, and the other is a pointer to a qualified or unqualified version of void, and the type pointed to by the left has all the qualifiers of the type pointed to by the right;
  • the left operand is an atomic, qualified, or unqualified pointer, and the right is a null pointer constant; or
  • the left operand has type atomic, qualified, or unqualified _Bool, and the right is a pointer.
112) The asymmetric appearance of these constraints with respect to type qualifiers is due to the conversion (specified in 6.3.2.1) that changes lvalues to “the value of the expression” and thus removes any type qualifiers that were applied to the type category of the expression (for example, it removes const but not volatile from the type int volatile * const).
C11 § 6.3.2.1 Lvalues, arrays, and function designators ¶ 2
[…] lvalue conversion. If the lvalue has qualified type, the value has the unqualified version of the type of the lvalue […]
Except when it is the operand of the sizeof operator, the _Alignof operator, the unary & operator, the ++ operator, the -- operator, or the left operand of the . operator or an assignment operator, an lvalue that does not have array type is converted to the value stored in the designated object (and is no longer an lvalue); this is called lvalue conversion. If the lvalue has qualified type, the value has the unqualified version of the type of the lvalue; additionally, if the lvalue has atomic type, the value has the non-atomic version of the type of the lvalue; otherwise, the value has the type of the lvalue. If the lvalue has an incomplete type and does not have array type, the behavior is undefined. If the lvalue designates an object of automatic storage duration that could have been declared with the register storage class (never had its address taken), and that object is uninitialized (not declared with an initializer and no assignment to it has been performed prior to use), the behavior is undefined.

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.

For any qualifier 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).