Search code examples
clanguage-lawyerconst-pointer

Implicit and explicit conversion of pointers to const objects in C


The standard clearly states that attempting to access a const object through a pointer to a non-const type of that object will cause Undefined Behavior. The same applies to objects qualified with the volatile keyword and pointers to such objects.

But what is meant by access? For example, is an arbitrarily long string of pointer precasts containing an "incorrect" cast, but finally convertible to the correct type, an expression

int const i = 0;
int const *p = &i; // ok
int const *t = (int const *)(float *)(int *)&i; // is this OK or UB?

I have always been tormented by this point according to the strictness of the C language standard. In particular, I have many of the following special cases of passing arguments to functions, where they must be explicitly cast:

void send(unsigned int const buf[], unsigned int size);

typedef struct {
  unsigned int hdr;
  float temp;
  unsigned int data[5];
} Str;

void func(void) {
  Str const s = {
    0, 1.5f, {0, 1, 2, 3, 4}
  };
  ...
  send((unsigned int const *)&s, sizeof(s));
}

According to the rules of the standard, I can safely convert a pointer to a structure to a pointer to its first element (with the appropriate type), but nothing is said about additional obligations with qualifiers (or I did not see it). While I understand formally that it is correct to leave const in an explicit cast, it often clutters up the code a lot, and I would like to omit the use of const in this expression. I want to be sure that calling

send((unsigned int *)&s, sizeof(s));

is as safe as calling

send((unsigned int const *)&s, sizeof(s));

since the prototype of the send() function will explicitly force the compiler to implicitly convert (unsigned int *) to (unsigned int const *) for me.

But if any intermediate loss of the const qualifier in type conversions (in type casts), despite the fact that even the final type will have the correct qualifier (a conversion of the form (type const *) -> (type *) -> ... -> (type const *)), is prohibited by the standard, then a call in the style of send((unsigned int *)&s, sizeof(s)); will be incorrect.

Please confirm or deny with the points of the C standard.


Solution

  • But what is meant by access?

    C 2024 3.1 defines access:

    ⟨execution-time action⟩ read or modify the value of an object

    (Earlier versions of the standard said “to read or modify”.)

    For example, is an arbitrarily long string of pointer precasts containing an "incorrect" cast, but finally convertible to the correct type…

    Converting pointers is merely operating on values and does not access the pointed-to objects.

    int const *t = (int const *)(float *)(int *)&i; // is this OK or UB?

    These involve rules about conversions of pointers, which I do not believe is the gist of your question. Per C 2024 6.3.3.3, conversions between pointers to object types are generally defined provided the alignment is suitable for the destination type. &i is necessarily suitably aligned for an int, because it was defined with int const i, so the conversions to int * and int const * are defined. The conversion to float * is defined if i is suitably aligned for a float. (An int is always suitably aligned for float in most common C implementations.)

    Given satisfactory alignment, conversion to a different pointer to an object type and back to the original type is guaranteed to produce a value equal to the original. Chains of conversions are not explicitly specified but may be reasonably presumed to restore the original value.

    The standard clearly states that attempting to access a const object through a pointer to a non-const type of that object will cause Undefined Behavior.

    It does not say that. As we see above, “access” includes reading, and reading an object defined with const is defined. What the standard says is undefined in this regard is, per 6.7.4.1, modifying a const-qualified object:

    If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined.

    Also note that “undefined behavior” is not a proper noun and should not be capitalized.

    I want to be sure that calling send((unsigned int *)&s, sizeof(s)); is as safe as calling send((unsigned int const *)&s, sizeof(s));

    Both of these are equivalent in terms of what the C standard specifies for execution in its abstract machine; two programs differing only in this code will have the same execution behavior (barring compounding factors external to this, such as somebody examining the source code and entering different inputs to the different programs).

    However, a compiler could warn that the former casts away const while it would not warn for the latter. This is permitted by the standard (5.2.1.3: “Of course, an implementation is free to produce any number of diagnostic messages, often referred to as warnings, as long as a valid program is still correctly translated.”), so it is not guaranteed the two programs will behave identically in this regard.

    Further, a large portion of programming is not merely how programs interact with compilers and computers but how they interact with humans. One piece of code may confuse future human readers more than the other.