¿Se garantiza que las estructuras C con los mismos tipos de miembros tengan el mismo diseño en la memoria?

13 minutos de lectura

avatar de usuario
Soumya

Esencialmente, si tengo

typedef struct {
    int x;
    int y;
} A;

typedef struct {
    int h;
    int k;
} B;

y yo tengo A a¿el estándar C garantiza que ((B*)&a)->k es lo mismo que a.y?

  • No, no creo que el estándar garantice eso. En la práctica, los compiladores lo harán como quieras y esperes, pero el estándar no lo garantiza. Es un comportamiento indefinido; Cualquier cosa podría pasar.

    –Jonathan Leffler

    6 de noviembre de 2013 a las 5:48

¿Se garantiza que las estructuras C con los mismos tipos de miembros tengan el mismo diseño en la memoria?

Casi si. Lo suficientemente cerca para mí.

De n1516, Sección 6.5.2.3, párrafo 6:

… si una unión contiene varias estructuras que comparten una secuencia inicial común …, y si el objeto de la unión actualmente contiene una de estas estructuras, se permite inspeccionar la parte inicial común de cualquiera de ellas en cualquier lugar que una declaración de la el tipo completo de la unión es visible. Dos estructuras comparten un secuencia inicial común si los miembros correspondientes tienen tipos compatibles (y, para campos de bits, los mismos anchos) para una secuencia de uno o más miembros iniciales.

Esto significa que si tienes el siguiente código:

struct a {
    int x;
    int y;
};

struct b {
    int h;
    int k;
};

union {
    struct a a;
    struct b b;
} u;

Si asignas a u.ael estándar dice que puede leer los valores correspondientes de u.b. Estira los límites de la plausibilidad para sugerir que struct a y struct b puede tener un diseño diferente, dado este requisito. Tal sistema sería patológico en extremo.

Recuerda que la norma también garantiza que:

  • Las estructuras nunca son representaciones trampa.

  • Las direcciones de los campos en una estructura aumentan (a.x siempre es antes a.y).

  • El desplazamiento del primer campo siempre es cero.

Sin embargo, y esto es importante!

Reformulaste la pregunta,

¿El estándar C garantiza que ((B*)&a)->k es lo mismo que ay?

¡No! ¡Y dice muy explícitamente que no son lo mismo!

struct a { int x; };
struct b { int x; };
int test(int value)
{
    struct a a;
    a.x = value;
    return ((struct b *) &a)->x;
}

Esta es una violación de alias.

  • ¿Por qué N1516? Me refiero al N1570…

    – Patatas

    6 de noviembre de 2013 a las 6:04

  • @Potatoswatter: Es lo que tenía por ahí. El lenguaje ha estado allí desde los días de ANSI C de todos modos (sección 3.3.2.3).

    -Dietrich Epp

    6 de noviembre de 2013 a las 6:07

  • Si una unión completa escribe declaración que contiene ambos struct a y struct b es visible donde el código inspecciona el miembro de la estructura, un conforme y el compilador sin errores reconocerá la posibilidad de creación de alias. Algunos escritores de compiladores que solo quieren cumplir con el estándar cuando les conviene romperán dicho código aunque el estándar garantice que funcionará; eso simplemente significa que sus compiladores no están conformes.

    – Super gato

    8 de agosto de 2017 a las 23:39

  • @supercat Sí, pero no un solo compilador (que usa un alias estricto durante la optimización) que conozco implementa esta regla, por lo que no se puede confiar en ella. En el futuro, esta cláusula podría eliminarse. Los estándares son en su mayoría basura de todos modos, la mayoría de los compiladores realmente no los siguen.

    – StaceyGirl

    26 de marzo de 2018 a las 16:15


  • @wonder.mice: No es suficiente que x tiene el mismo tipo en ambos. El problema es ese a tiene tipo struct ay está accediendo a él a través de un tipo de struct b. Aquí hay un enlace que le muestra cómo se optimizará un compilador en función de los alias: gcc.godbolt.org/z/7PMjbT intente eliminar -fstrict-aliasing y viendo como cambia el código generado.

    -Dietrich Epp

    14 de febrero de 2020 a las 20:29

avatar de usuario
pestaña

Aprovechando las otras respuestas con una advertencia sobre la sección 6.5.2.3. Aparentemente, existe cierto debate sobre la redacción exacta de anywhere that a declaration of the completed type of the union is visibley al menos GCC no lo implementa como está escrito. Hay algunos informes de defectos tangenciales de C WG aquí y aquí con comentarios de seguimiento del comité.

Recientemente traté de averiguar cómo otros compiladores (específicamente GCC 4.8.2, ICC 14 y clang 3.4) interpretan esto usando el siguiente código del estándar:

// Undefined, result could (realistically) be either -1 or 1
struct t1 { int m; } s1;
struct t2 { int m; } s2;
int f(struct t1 *p1, struct t2 *p2) {
    if (p1->m < 0)
        p2->m = -p2->m;
    return p1->m;
}
int g() {
    union {
        struct t1 s1;
        struct t2 s2;
    } u;
    u.s1.m = -1;
    return f(&u.s1,&u.s2);
}

CCG: -1, sonido metálico: -1, CPI: 1 y advierte sobre la violación de alias

// Global union declaration, result should be 1 according to a literal reading of 6.5.2.3/6
struct t1 { int m; } s1;
struct t2 { int m; } s2;
union u {
    struct t1 s1;
    struct t2 s2;
};
int f(struct t1 *p1, struct t2 *p2) {
    if (p1->m < 0)
        p2->m = -p2->m;
    return p1->m;
}
int g() {
    union u u;
    u.s1.m = -1;
    return f(&u.s1,&u.s2);
}

CCG: -1, sonido metálico: -1, CPI: 1 pero advierte sobre la violación de aliasing

// Global union definition, result should be 1 as well.
struct t1 { int m; } s1;
struct t2 { int m; } s2;
union u {
    struct t1 s1;
    struct t2 s2;
} u;
int f(struct t1 *p1, struct t2 *p2) {
    if (p1->m < 0)
        p2->m = -p2->m;
    return p1->m;
}
int g() {
    u.s1.m = -1;
    return f(&u.s1,&u.s2);
}

CCG: -1, sonido metálico: -1, CPI: 1, sin advertencia

Por supuesto, sin optimizaciones de alias estrictas, los tres compiladores devuelven el resultado esperado cada vez. Dado que clang y gcc no tienen resultados distinguidos en ninguno de los casos, la única información real proviene de la falta de diagnóstico de ICC en el último. Esto también se alinea con el ejemplo dado por el comité de estándares en el primer informe de defectos mencionado anteriormente.

En otras palabras, este aspecto de C es un verdadero campo minado, y deberá tener cuidado de que su compilador esté haciendo lo correcto, incluso si sigue el estándar al pie de la letra. Peor aún, ya que es intuitivo que un par de estructuras de este tipo deberían ser compatibles en la memoria.

  • Muchas gracias por los enlaces, aunque lamentablemente son en gran medida intrascendentes. Por poco que pueda valer, el consenso entre las pocas personas (laicas) con las que he discutido esto parece ser que significa que la función debe pasar el union, no punteros sin formato a los tipos contenidos. Esto, sin embargo, anula el punto de usar un union en primer lugar, en mi opinión. Tengo una pregunta sobre esta cláusula, específicamente su exclusión notable (¿y quizás accidental?) de C++, aquí: stackoverflow.com/q/34616086/2757035

    – subrayado_d

    6 de enero de 2016 a las 12:45

  • ¡Nada de intrascendente! A través de una segunda discusión de GCC vinculada desde la suya, vemos que C ++ puede haber rechazado esto deliberadamente, mientras que C realmente no pensó antes de agregar esta redacción, nunca lo tomó en serio y podría estar revirtiéndolo: gcc.gnu.org/bugzilla/show_bug.cgi?id=65892 A partir de ahí, llegamos a C++ DR 1719 open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1719 lo que sugiere un cambio de redacción importante que parece hacer que la perspectiva de C ++ sobre exactamente dónde structs lata ser ‘juego de palabras’ muy claro. He recopilado esto y mucho más en una respuesta a mi pregunta vinculada

    – subrayado_d

    06/01/2016 a las 19:44


  • @curiousguy: para que la regla CIS sea útil en compiladores que no pueden reconocer el acto de derivar un puntero o lvalue de un tipo a partir de un puntero o lvalue de otro en secuencia en relación con otras acciones que involucran esos tipos, debe haber un medio de decirle al compilador “este puntero identificará uno de estos tipos de estructura, y no sé cuál, pero necesito poder usar los miembros CIS de uno para acceder a los miembros CIS de todos ellos”. Hacer que las declaraciones de unión sirvan para ese propósito además de declarar los tipos de unión evitaría la necesidad de introducir una nueva directiva…

    – Super gato

    30 mayo 2018 a las 19:45

  • …para ese propósito. Tenga en cuenta que la forma en que se escribe 6.5p7, dado struct foo {int x;} *p, it;algo como p=&it; p->x=4; invocaría a UB ya que usa un lvalue de tipo int para modificar un objeto de tipo struct foo, pero los autores del Estándar esperan que los escritores de compiladores no sean tan obtusos como para pretender que no deberían tratar eso como está definido. El Estándar nunca ha hecho ningún intento razonable de especificar completamente la gama completa de semántica que debería ser compatible con una implementación dirigida a cualquier plataforma y propósito en particular. Las absurdas reglas de “tipo efectivo” ni siquiera pueden…

    – Super gato

    30 de mayo de 2018 a las 19:52

  • … manejar las operaciones más básicas en miembros de estructura de tipos que no son de carácter. Si uno modificara 6.5p7 para decir que se debe acceder a cualquier byte de almacenamiento que se cambie durante cualquier ejecución particular de una función o bucle durante su vida útil exclusivamente a través de valores l que se derivan, durante esa ejecución, del mismo objeto o elementos de la misma matriz, y que todo uso de un lvalue derivado en relación con un byte precede al siguiente uso del padre en relación con ese byte, uno podría deshacerse de todo lo que tiene que ver con “tipos efectivos” y hacer las cosas más simples y más poderoso.

    – Super gato

    30 mayo 2018 a las 19:56

Este tipo de alias requiere específicamente un union escribe. C11 §6.5.2.3/6:

Se hace una garantía especial para simplificar el uso de uniones: si una unión contiene varias estructuras que comparten una secuencia inicial común (ver más abajo), y si el objeto de unión actualmente contiene una de estas estructuras, se permite inspeccionar la parte inicial común de cualquiera de ellos en cualquier lugar que sea visible una declaración del tipo completo de la unión. Dos estructuras comparten una secuencia inicial común si los miembros correspondientes tienen tipos compatibles (y, para campos de bits, los mismos anchos) para una secuencia de uno o más miembros iniciales.

Este ejemplo sigue:

El siguiente no es un fragmento válido (porque el tipo de unión no es visible dentro de la función f):

struct t1 { int m; };
struct t2 { int m; };
int f(struct t1 *p1, struct t2 *p2)
{
    if (p1->m < 0)
          p2->m = -p2->m;
    return p1->m;
}

int g() {
    union {
          struct t1 s1;
          struct t2 s2;
    } u;
    /* ... */
    return f(&u.s1, &u.s2);}
}

Los requisitos parecen ser que 1. el objeto al que se le asigna un alias se almacena dentro de un union y 2. que la definición de ese union el tipo está dentro del alcance.

Por si sirve de algo, la correspondiente relación inicial-subsecuencia en C++ no requiere un union. Y en general, tal union la dependencia sería un comportamiento extremadamente patológico para un compilador. Si hay alguna forma en que la existencia de un tipo de unión podría afectar un modelo de memoria concreto, probablemente sea mejor no tratar de imaginarlo.

Supongo que la intención es que un verificador de acceso a la memoria (piense en Valgrind con esteroides) pueda verificar un posible error de alias en contra de estas reglas “estrictas”.

  • Es posible que C ++ no estipule que se requiere la declaración de unión, pero aún así se comporta de manera idéntica a C, no permitiendo la creación de alias en punteros ‘desnudos’ para union miembros, a través de GCC y Clang. Consulte la prueba de @ecatmur sobre mi pregunta aquí sobre por qué esta cláusula se dejó fuera de C ++: stackoverflow.com/q/34616086/2757035 Cualquier idea que los lectores puedan tener sobre esta diferencia sería muy bienvenida. Sospecho que esta cláusula deberían agregarse a C++ y se omitió accidentalmente por ‘herencia’ de C99, donde se agregó (C99 no lo tenía).

    – subrayado_d

    06/01/2016 a las 12:35


  • @underscore_d La parte de visibilidad se omitió deliberadamente de C ++ porque se considera ampliamente ridículo e inimplementable (o al menos distante de las consideraciones prácticas de cualquier implementación). El análisis de alias es parte del back-end del compilador, y la visibilidad de la declaración generalmente solo se conoce en el front-end.

    – Patatas

    6 de enero de 2016 a las 15:44

  • @underscore_d La gente en esa discusión está esencialmente “en el registro” allí. Andrew Pinski es un tipo incondicional de backend de GCC. Martin Sebor es un miembro activo del comité C. Jonathan Wakely es un miembro activo del comité de C++ e implementador de lenguaje/biblioteca. Esa página es más autorizada, clara y completa que cualquier cosa que pueda escribir.

    – Patatas

    6 de enero de 2016 a las 16:09

  • @underscore_d La intención de N685 no es particularmente clara, ya que no profundiza mucho en por qué las palabras propuestas realmente resuelven el problema. C++, que omite la redacción N685, también está indeciso (o tal vez finalmente llegue a un consenso) en cuanto a lo que se puede hacer con los punteros en la subsecuencia inicial. La cita del reflector muestra a alguien que deriva las reglas adecuadas de los aspectos prácticos, no del estándar. Los comités de C y C++ (a través de Martin y Clark) intentarán encontrar un consenso y elaborarán una redacción para que el estándar finalmente pueda decir lo que significa.

    – Patatas

    6 de enero de 2016 a las 17:09

  • … que los autores no tenían la intención de que 6.5p7 describiera completamente todos los casos que los compiladores deberían admitir. En cambio, esperaban que los escritores de compiladores pudieran juzgar mejor las situaciones en las que deberían reconocer un acceso a un puntero derivado o lvalue como un acceso o acceso potencial al valor original. El problema es que algunos escritores de compiladores han tenido una idea distorsionada de que el Estándar alguna vez tuvo la intención de describir completamente todos los comportamientos que los programadores deberían esperar de calidad implementaciones, a pesar de que la justificación deja en claro que ese no fue el caso.

    – Super gato

    30 de mayo de 2018 a las 14:48

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad