¿Cuál es la razón para que el estándar C considere la constidad recursivamente?

9

El estándar C99 dice en 6.5.16: 2:

Un operador de asignación tendrá un valor l modificable como su operando izquierdo.

y en 6.3.2.1:1:

Un lvalue modificable es un lvalue que no tiene un tipo de matriz, no tiene un tipo incompleto, no tiene un tipo const y, si es una estructura o unión, no tiene ningún miembro (incluido, recursivamente, ningún miembro o elemento de todos los agregados o uniones contenidos) con un tipo calificado const.

Ahora, consideremos un no const structcon un constcampo.

typedef struct S_s {
    const int _a;
} S_t;

Por norma, el siguiente código es comportamiento indefinido (UB):

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

El problema semántico con esto es que la entidad que la encierra ( struct) debe considerarse escribible (no solo de lectura), a juzgar por el tipo declarado de la entidad ( S_t s1), pero no debe considerarse escribible por la redacción del estándar (las 2 cláusulas en la parte superior) debido al constcampo _a. El estándar no deja claro para un programador que lee el código que la asignación es en realidad una UB, porque es imposible decir que sin la definición de struct S_s ... S_ttipo.

Además, el acceso de solo lectura al campo solo se aplica sintácticamente de todos modos. No hay forma de que algunos constcampos que no const structsean realmente se coloquen en almacenamiento de solo lectura. Pero tal redacción de los proscritos estándar del código que desecha deliberadamente el constcalificador de campos en los procedimientos de acceso de estos campos, así ( ¿Es una buena idea consolidar los campos de estructura en 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);
}

Entonces, por alguna razón, para que un todo structsea ​​de solo lectura es suficiente declararloconst

const S_t s3;

Pero para que un todo structno sea de solo lectura, no es suficiente declararlo sin él const.

Lo que creo que sería mejor es:

  1. Para restringir la creación de no constestructuras con constcampos, y emitir un diagnóstico en tal caso. Eso dejaría en claro que los structcampos que contienen solo lectura son solo de lectura.
  2. Para definir el comportamiento en caso de escribir en un constcampo perteneciente a una constestructura no estructurada para que el código anterior (*) cumpla con el Estándar.

De lo contrario, el comportamiento no es consistente y difícil de entender.

Entonces, ¿cuál es la razón por la cual C Standard considera la constrecursividad recursivamente, como dice?

Michael Pankov
fuente
Para ser honesto, no veo una pregunta allí.
Bart van Ingen Schenau
@BartvanIngenSchenau editó para agregar la pregunta planteada en el tema al final del cuerpo
Michael Pankov
1
¿Por qué el voto negativo?
Michael Pankov

Respuestas:

4

Entonces, ¿cuál es la razón para que C Standard considere la consistencia recursivamente, como dice?

Desde una perspectiva de tipo solo, no hacerlo sería poco sólido (en otras palabras: terriblemente roto e intencionalmente poco confiable).

Y eso se debe a lo que "=" significa en una estructura: es una asignación recursiva. De esto se deduce que eventualmente s1._a = <value>ocurre "dentro de las reglas de escritura". Si el estándar permite esto para los constcampos "anidados" , está agregando una seria inconsistencia en su definición del sistema de tipos como una contradicción explícita (también podría constdescartar la característica, ya que se volvió inútil y poco confiable por su propia definición).

Su solución (1), por lo que yo entiendo, está forzando innecesariamente a que toda la estructura sea constcada vez que uno de sus campos es const. De esta manera, s1._b = bsería ilegal para un ._bcampo no constante en un campo no constante que s1contiene un const a.

Thiago Silva
fuente
Bien. CApenas tiene un sistema de tipo de sonido (más como un montón de cajas de esquina atadas entre sí a lo largo de los años). Además, la otra forma de asignar la tarea a a structes memcpy(s_dest, s_src, sizeof(S_t)). Y estoy bastante seguro de que es la forma en que se implementa. Y en tal caso, incluso el "sistema de tipos" existente no le prohíbe hacerlo.
Michael Pankov
2
Muy cierto. Espero no haber implicado que el sistema de tipos de C sea sólido, solo que hacer que una semántica específica no sea válida lo derrota deliberadamente. Además, aunque el sistema de tipos de C no se aplica con fuerza, las formas de violarlo a menudo son explícitas (punteros, acceso indirecto, lanzamientos), a pesar de que sus efectos son con frecuencia implícitos y difíciles de rastrear. Por lo tanto, tener "cercas" explícitas para romperlas informa mejor que tener una contradicción en las definiciones mismas.
Thiago Silva
2

La razón es que los campos de solo lectura son de solo lectura. No hay gran sorpresa allí.

Asume erróneamente que el único efecto está en la colocación en ROM, lo que de hecho es imposible cuando hay campos adyacentes no constantes. En realidad, los optimizadores pueden asumir que las constexpresiones no están escritas y optimizar en función de eso. Por supuesto, esa suposición no se cumple cuando existen alias no constantes.

Su solución (1) rompe el código legal y razonable existente. Eso no va a suceder. Su solución (2) prácticamente elimina el significado de constlos miembros. Si bien esto no romperá el código existente, parece carecer de una justificación.

MSalters
fuente
Estoy 90% seguro de que los optimizadores no pueden asumir que los constcampos no están escritos, porque siempre puedes usar memseto memcpy, y eso incluso sería compatible con el Estándar. (1) puede implementarse como, como mínimo, advertencia adicional, habilitada por una bandera. La justificación de (2) es que, bueno, exactamente, no hay forma de que un componente structpueda considerarse no escribible cuando toda la estructura es escribible.
Michael Pankov
Un "diagnóstico opcional determinado por una bandera" sería un requisito único para que lo exigiera el Estándar. Además, establecer la bandera aún rompería el código existente, por lo que en efecto nadie se molestaría con la bandera y sería un callejón sin salida. En cuanto a (2), 6.3.2.1:1 especifica exactamente lo contrario: toda la estructura es no se puede escribir cuando un componente lo es. Sin embargo, otros componentes aún pueden ser grabables. Cf. C ++, que también se define operator=en términos de los miembros, y por lo tanto no define operator=cuándo es un miembro const. C y C ++ siguen siendo compatibles aquí.
MSalters
@constantius: el hecho de que PUEDES hacer algo para evitar deliberadamente la constidad de un miembro NO es una razón para que el optimizador ignore esa constidad. PUEDES desechar la constidad dentro de una función, lo que te permite cambiar cosas. Pero el optimizador en el contexto de llamada todavía puede asumir que no lo hará. Constness es útil para el programador, pero en algunos casos también es un envío de Dios para el optimizador.
Michael Kohne
Entonces, ¿por qué una estructura no editable se puede sobrescribir con ie memcpy? En cuanto a otras razones, está bien, es legado, pero ¿por qué se hizo de esa manera en primer lugar?
Michael Pankov
1
Todavía me pregunto si tu comentario sobre memcpyes correcto. AFACITO La cita de John Bode en su otra pregunta es correcta: su código escribe en un objeto calificado const y, por lo tanto, NO es una queja estándar, fin de la discusión.
MSalters