c++ c struct unions type-punning

estructuras ''punning'' de unión con “secuencia inicial común”: ¿Por qué C(99+), pero no C++, estipula una ''declaración visible del tipo de unión''?



unions type-punning (2)

Fondo

Las discusiones sobre la naturaleza mayormente no definida o implementada de la escritura de tipos a través de una union generalmente citan los siguientes bits, aquí a través de @ecatmur ( https://stackoverflow.com/a/31557852/2757035 ), sobre una exención para el estándar struct distribución que tienen una "secuencia inicial común" de tipos de miembros:

C11 ( 6.5.2.3 Estructura y miembros del sindicato ; Semántica ):

[...] 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 ellas en cualquier lugar que La declaración del tipo completo de la unión es visible . 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.

C ++ 03 ( [class.mem] / 16 ):

Si una unión POD contiene dos o más estructuras POD que comparten una secuencia inicial común, y si el objeto de unión POD actualmente contiene una de estas estructuras POD, se permite inspeccionar la parte inicial común de cualquiera de ellas. Dos estructuras POD comparten una secuencia inicial común si los miembros correspondientes tienen tipos compatibles con el diseño (y, para campos de bits, los mismos anchos) para una secuencia de uno o más miembros iniciales.

Otras versiones de los dos estándares tienen un lenguaje similar; desde C ++ 11 la terminología utilizada es el diseño estándar en lugar de POD .

Como no se requiere una reinterpretación, esto no es realmente un tipo de letra, solo la sustitución de nombre aplicada a los accesos de miembros del union . Una propuesta para C ++ 17 (el infame P0137R1) lo hace explícito usando un lenguaje como ''el acceso es como si el otro miembro de la estructura estuviera nominado''.

Pero tenga en cuenta la negrita: "en cualquier lugar donde se vea una declaración del tipo completo de la unión ", una cláusula que existe en C11 pero en ninguna parte de los borradores de C ++ para 2003, 2011 o 2014 (todas casi idénticas, pero las versiones posteriores reemplazan " POD "con el nuevo término diseño estándar ). En cualquier caso, la ''declaración visible del bit de tipo union está totalmente ausente en la sección correspondiente de cualquier estándar de C ++.

@loop y @ Mints97, aquí - https://stackoverflow.com/a/28528989/2757035 - muestran que esta línea también estuvo ausente en C89, apareciendo por primera vez en C99 y permaneciendo en C desde entonces (aunque, de nuevo, nunca se filtró a través de a C ++).

Discusiones sobre estándares alrededor de esto

[recortado - mira mi respuesta]

Preguntas

A partir de esto, entonces, mis preguntas fueron:

  • ¿Qué significa esto? ¿Qué se clasifica como una ''declaración visible''? ¿Esta cláusula tenía la intención de reducir, o ampliar, el rango de contextos en los que ese ''castigo'' ha definido el comportamiento?

  • ¿Debemos suponer que esta omisión en C ++ es muy deliberada?

  • ¿Cuál es la razón para que C ++ difiera de C? ¿C ++ simplemente ''heredó'' esto de C89 y luego decidió, o peor aún, se olvidó , actualizar junto con C99?

  • Si la diferencia es intencional, ¿qué beneficios o inconvenientes hay para los 2 tratamientos diferentes en C vs C ++?

  • ¿Qué ramificaciones interesantes, si es que tiene, tiene en tiempo de compilación o tiempo de ejecución? Por ejemplo, @ecatmur, en un comentario respondiendo a mi señalando esto en su respuesta original (enlace como arriba), especuló de la siguiente manera.

Me imagino que permite una optimización más agresiva; C puede suponer que los argumentos de función S* s T* t no tienen alias, incluso si comparten una secuencia inicial común siempre que no haya union { S; T; } union { S; T; } union { S; T; } está a la vista, mientras que C ++ puede hacer esa suposición solo en el momento del enlace. Podría valer la pena hacer una pregunta por separado sobre esa diferencia.

Bueno, aquí estoy, preguntando! Estoy muy interesado en cualquier idea sobre esto, especialmente: otras partes relevantes del Estándar (cualquiera), citas de miembros del comité u otros comentaristas estimados, ideas de desarrolladores que podrían haber notado una diferencia práctica debido a esto, suponiendo que cualquier compilador incluso se molesta en hacer cumplir la cláusula adicional de C - y etc. El objetivo es generar un catálogo útil de hechos relevantes sobre esta cláusula C y su omisión (intencional o no) de C ++. ¡Entonces vamos!


He encontrado mi camino a través del laberinto a algunas excelentes fuentes sobre esto, y creo que tengo un resumen bastante completo de ello. Estoy publicando esto como una respuesta porque parece explicar tanto la intención (IMO muy equivocada) de la cláusula C como el hecho de que C ++ no la hereda. Esto evolucionará con el tiempo si descubro más material de apoyo o la situación cambia.

Esta es la primera vez que intento resumir una situación muy compleja, que parece estar mal definida incluso para muchos arquitectos de idiomas, por lo que agradeceré aclaraciones / sugerencias sobre cómo mejorar esta respuesta, o simplemente una mejor respuesta si alguien tiene una.

Finalmente, algunos comentarios concretos

A través de hilos vagamente relacionados, encontré la siguiente respuesta de @tab, y aprecié mucho los enlaces contenidos a los informes (defectuosos, si no concluyentes) de GCC y del Grupo de Trabajo: respuesta por pestaña en

El enlace del CCG contiene una discusión interesante y revela una cantidad considerable de confusión e interpretaciones conflictivas por parte del Comité y los vendedores del compilador, en torno al tema de union struct , castigos y alias de union miembros del union , tanto en C como en C ++.

Al final de eso, estamos vinculados al evento principal: otro hilo de BugZilla, Bug 65892 , que contiene una discusión extremadamente útil. En particular, nos encontramos con el primero de dos documentos fundamentales:

Origen de la línea agregada en C99

La propuesta C N685 es el origen de la cláusula adicional sobre la visibilidad de una declaración de tipo de union . A través de lo que algunos reclamos (ver el hilo del CCG # 2) es una interpretación errónea total de la asignación de "secuencia inicial común", N685 tenía la intención de permitir la relajación de las reglas de alias para struct "secuencia inicial común" dentro de una TU consciente de alguna union contiene instancias de dichos tipos de struct , como podemos ver en esta cita:

La solución propuesta es exigir que una declaración de unión sea visible si son posibles los alias a través de una secuencia inicial común (como la anterior). Por lo tanto, la siguiente TU proporciona este tipo de alias si se desea:

union utag { struct tag1 { int m1; double d2; } st1; struct tag2 { int m1; char c2; } st2; }; int similar_func(struct tag1 *pst2, struct tag2 *pst3) { pst2->m1 = 2; pst3->m1 = 0; /* might be an alias for pst2->m1 */ return pst2->m1; }

A juzgar por la discusión del CCG y los comentarios a continuación, como @ ecatmur, esta propuesta, que parece exigir especulativamente el alias para cualquier tipo de struct que tenga alguna instancia dentro de una union visible para esta TU, parece haber recibido una gran burla y rara vez se implementó .

Es obvio lo difícil que sería satisfacer esta interpretación de la cláusula adicional sin paralizar por completo muchas optimizaciones, por poco beneficio, ya que pocos codificadores desearían esta garantía, y aquellos que lo hacen pueden activar el fno-strict-aliasing (lo cual indica IMO problemas mayores). Si se implementa, es más probable que esta asignación atrape a las personas e interactúe espuriosamente con otras declaraciones de union , que ser útil.

Omisión de la línea de C ++

Siguiendo con esto y un comentario que hice en otra parte, @Potatoswatter en esta respuesta aquí en SO afirma que:

La parte de visibilidad se omitió intencionalmente de C ++ porque se considera ampliamente ridícula e implementable.

En otras palabras, parece que C ++ evitó deliberadamente adoptar esta cláusula adicional, probablemente debido a su absurdo ampliamente percibido. Al solicitar una cita "en el registro" de esto, Potatoswatter proporcionó la siguiente información clave sobre los participantes del hilo:

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

Potatoswatter, en el mismo hilo SO vinculado anteriormente, concluye que C ++ excluyó deliberadamente esta línea, sin dejar ningún tratamiento especial (o, en el mejor de los casos, tratamiento definido por la implementación) para los punteros en la secuencia inicial común. Queda por ver si su tratamiento se definirá específicamente en el futuro, en comparación con cualquier otro indicador; compárese con mi sección final a continuación sobre C. En la actualidad, sin embargo, no lo es (y nuevamente, en mi opinión, esto es bueno).

¿Qué significa esto para C ++ y las implementaciones prácticas de C?

Entonces, con la nefasta línea de N685 ... ''descartada'' ... volvemos a asumir que los punteros en la secuencia inicial común no son especiales en términos de alias. Todavía. Vale la pena confirmar lo que significa este párrafo en C ++ sin él. Bueno, el segundo hilo de GCC anterior enlaza con otra gema:

Defecto C ++ 1719 . Esta propuesta ha alcanzado el estado DRWP : "Un problema de DR cuya resolución se refleja en el Documento de trabajo actual. El Documento de trabajo es un borrador para una versión futura de la Norma" - cite . Esta es la publicación C ++ 14 o al menos después del borrador final que tengo aquí (N3797), y presenta una reescritura significativa, y en mi opinión esclarecedora, de la redacción de este párrafo , de la siguiente manera. Estoy en negrita lo que considero que son los cambios importantes, y {estos comentarios} son míos:

En una unión de diseño estándar con un miembro activo {"active" indica una instancia de union , no solo el tipo} (9.5 [class.union]) del tipo de estructura T1 , se permite leer {anteriormente "inspeccionar"} miembro de datos estáticos m de otro miembro de unión de tipo estructura T2 siempre que m sea ​​parte de la secuencia inicial común de T1 y T2 . [ Nota : leer un objeto volátil a través de un valor gl no volátil tiene un comportamiento indefinido (7.1.6.1 [dcl.type.cv]). —Final nota]

Esto parece aclarar el significado de la antigua redacción: para mí, dice que cualquier ''castigo'' específicamente permitido entre union struct miembros del union con secuencias iniciales comunes debe hacerse a través de una instancia de la union principal , en lugar de basarse en el tipo de las structs (por ejemplo, los punteros a ellas pasaron a alguna función). Esta redacción parece descartar cualquier otra interpretación, a la N685. C haría bien en adoptar esto, diría. Hola, hablando de eso, ¡mira abajo!

El resultado es que, como lo demostraron @ecatmur y en los tickets de GCC, esto deja tales struct miembros de la union por definición en C ++, y prácticamente en C, sujetas a las mismas estrictas reglas de alias que cualquier otro 2 punteros oficialmente no relacionados. La garantía explícita de poder leer la secuencia inicial común de struct miembros union inactivos está ahora más claramente definida, sin incluir la "visibilidad" vaga e inimaginablemente tediosa para hacer cumplir, como lo intentó N685 para C. Por esta definición, el principal los compiladores se han comportado como se esperaba para C ++. En cuanto a C?

Posible inversión de esta línea en C / clarificación en C ++

También vale la pena señalar que el miembro del comité C, Martin Sebor, también está tratando de arreglar esto en ese excelente lenguaje:

Martin Sebor 2015-04-27 14:57:16 UTC Si alguno de ustedes puede explicar el problema con él, estoy dispuesto a escribir un documento y presentarlo al WG14 y solicitar que se cambie el estándar.

Martin Sebor 2015-05-13 16:02:41 UTC Tuve la oportunidad de discutir este tema con Clark Nelson la semana pasada. Clark ha trabajado en la mejora de las partes de alias de la especificación C en el pasado, por ejemplo en N1520 ( http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1520.htm ). Estuvo de acuerdo en que, al igual que los problemas señalados en N1520, este también es un problema pendiente que valdría la pena que WG14 revise y solucione ".

Potatoswatter concluye inspiradoramente:

Los comités C y C ++ (a través de Martin y Clark) tratarán de encontrar un consenso y elaborar una redacción para que el estándar finalmente pueda decir lo que significa.

¡Solo podemos esperar!

Nuevamente, todos los pensamientos adicionales son bienvenidos.


Sospecho que significa que el acceso a estas partes comunes está permitido no solo a través del tipo de unión, sino también fuera de la unión. Es decir, supongamos que tenemos esto:

union u { struct s1 m1; struct s2 m2; };

Ahora suponga que en alguna función tenemos un puntero struct s1 *p1 que sabemos que se levantó del miembro m1 de dicha unión. Podemos convertir esto en un puntero de struct s2 * y aún acceder a los miembros que son comunes con la struct s1 . Pero en algún lugar del alcance, una declaración de union u debe ser visible. Y tiene que ser la declaración completa, que informa al compilador que los miembros son struct s1 y struct s2 .

La intención probable es que si existe un tipo de alcance de este tipo, entonces el compilador struct s1 que struct s1 y struct s2 tienen un alias, por lo que se sospecha que un acceso a través de un puntero struct s1 * realmente tiene acceso a un struct s2 o viceversa.

En ausencia de cualquier tipo de unión visible que se una a esos tipos de esta manera, no existe tal conocimiento; Se puede aplicar un alias estricto.

Dado que la redacción está ausente de C ++, para aprovechar la regla de "relajación de miembros iniciales comunes" en ese lenguaje, debe enrutar los accesos a través del tipo de unión, como se hace comúnmente de todos modos:

union u *ptr_any; // ... ptr_any->m1.common_initial_member = 42; fun(ptr_any->m2.common_initial_member); // pass 42 to fun