C Standard – Why Consider Const-ness Recursively?

cdesign

The C99 standard says in 6.5.16:2:

An assignment operator shall have a modifiable lvalue as its left
operand.

and in 6.3.2.1:1:

A modifiable lvalue is an lvalue that does not have array type, does
not have an incomplete type, does not have a const-qualified type, and
if it is a structure or union, does not have any member (including,
recursively, any member or element of all contained aggregates or
unions) with a const-qualified type.

Now, let's consider a non-const struct with a const field.

typedef struct S_s {
    const int _a;
} S_t;

By standard, the following code is undefined behavior (UB):

S_t s1;
S_t s2 = { ._a = 2 };
s1 = s2;

The semantic problem with this is that the enclosing entity (struct) should be considered writable (non-read-only), judging by the declared type of the entity (S_t s1), but should not be considered writable by the wording of standard (the 2 clauses on the top) because of const field _a. The Standard makes it unclear for a programmer reading the code that the assignment is actually a UB, because it's impossible to tell that w/o the definition of struct S_s ... S_t type.

Moreover, the read-only access to the field is only enforced syntactically anyway. There's no way some const fields of non-const struct are going really be placed to read-only storage. But such wording of standard outlaws the code which deliberately casts away the const qualifier of fields in accessor procedures of these fields, like so (Is it a good idea to const-qualify the fields of structure in C?):

(*)

#include <stdlib.h>
#include <stdio.h>

typedef struct S_s {
    const int _a;
} S_t;

S_t *
create_S(void) {
    return calloc(sizeof(S_t), 1);
}

void
destroy_S(S_t *s) {
    free(s);
}

const int
get_S_a(const S_t *s) {
    return s->_a;
}

void
set_S_a(S_t *s, const int a) {
    int *a_p = (int *)&s->_a;
    *a_p = a;
}

int
main(void) {
    S_t s1;
    // s1._a = 5; // Error
    set_S_a(&s1, 5); // OK
    S_t *s2 = create_S();
    // s2->_a = 8; // Error
    set_S_a(s2, 8); // OK

    printf("s1.a == %d\n", get_S_a(&s1));
    printf("s2->a == %d\n", get_S_a(s2));

    destroy_S(s2);
}

So, for some reason, for an entire struct to be read-only it's enough to declare it const

const S_t s3;

But for an entire struct to be non-read-only it's not enough to declare it w/o const.

What I think would be better, is either:

  1. To constrain the creation of non-const structures with const fields, and issue a diagnostic in such a case. That would make it clear that the struct containing read-only fields is read-only itself.
  2. To define the behavior in case of write to a const field belonging to a non-const struct as to make the code above (*) compliant to the Standard.

Otherwise the behavior is not consistent and hard to understand.

So, what's the reason for C Standard to consider const-ness recursively, as it puts it?

Best Answer

So, what's the reason for C Standard to consider const-ness recursively, as it puts it?

From a type perspective alone, not doing so would be unsound (in other words: terribly broken and intentionally unreliable).

And that's because of what "=" means on a struct: it is a recursive assignment. It follows that eventually you have a s1._a = <value> happening "inside the typing rules". If the standard allows this for "nested" const fields, its adding a serious inconsistency in its type system definition as an explicit contradiction (might as well throw the const feature away, as it just became useless and unreliable by its very definition).

Your solution (1), as far as I understand it, is unnecessarily forcing the entire structure to be const whenever one of its fields is const. In this way, s1._b = b would be illegal for a non-const ._b field on a non-const s1 containing a const a.