Estructuras de “juego de palabras” de unión con “secuencia inicial común”: ¿Por qué C (99+), pero no C ++, estipula una “statement visible del tipo de unión”?

Fondo

Las discusiones sobre la naturaleza, en su mayor parte, no definida o implementada por la implementación, de los tipos-juego de palabras a través de una union suelen citar los siguientes bits, aquí a través de @ecatmur ( https://stackoverflow.com/a/31557852/2757035 ), en una exención para estándar -layout struct s que tiene una “secuencia inicial común” de tipos de miembros:

C11 ( 6.5.2.3 Estructura y miembros de unión ; Semántica ):

[…] si una unión contiene varias estructuras que comparten una secuencia inicial común (ver a continuación), y si el objeto de unión contiene actualmente una de estas estructuras, se permite inspeccionar la parte inicial común de cualquiera de ellas en cualquier lugar que una la statement 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 los 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 POD-union contiene actualmente una de estas estructuras POD, se le 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 los 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 diseño estándar en lugar de POD .

Como no se requiere una reinterpretación, esto no es realmente un tipo de juego de palabras, solo sustitución de nombres aplicada a los accesos de los miembros de la 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 fuera nominado’.

Pero tenga en cuenta el negrita – “en cualquier lugar que sea visible una statement del tipo completo de la unión ” – una cláusula que existe en C11 pero en ninguna parte en los borradores de C ++ para 2003, 2011 o 2014 (todas casi idénticas, pero las versiones posteriores reemplazan ” POD “con el nuevo diseño estándar de término). En cualquier caso, la ‘statement visible de bit de tipo de 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 estaba ausente en C89, apareciendo por primera vez en C99 y permaneciendo en C desde entonces (aunque, de nuevo, nunca se filtra a través de a C ++).

Discusiones de estándares alrededor de esto

[recortado – ver mi respuesta]

Preguntas

De esto, entonces, mis preguntas fueron:

  • ¿Qué significa esto? ¿Qué se clasifica como una “statement visible”? ¿Esta cláusula tenía la intención de reducir o ampliar el rango de contextos en los que ese “juego de palabras” tiene un comportamiento definido?

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

  • ¿Cuál es la razón de que C ++ difiera de C? ¿C ++ simplemente ‘hereda’ esto de C89 y luego decide, o peor, olvida , actualizar junto con C99?

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

  • ¿Qué ramificaciones interesantes tiene, si es que tiene alguna, en comstackción o en tiempo de ejecución? Por ejemplo, @ecatmur, en un comentario que responde a mi señalamiento en su respuesta original (enlace como el anterior), especulado 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 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 tiempo de enlace. Puede valer la pena hacer una pregunta por separado sobre esa diferencia.

Bueno, aquí estoy, preguntando! Estoy muy interesado en cualquier idea al respecto, especialmente: otras partes relevantes del (estándar), citas de miembros del comité u otros comentaristas apreciados, ideas de desarrolladores que podrían haber notado una diferencia práctica debido a esto, suponiendo que cualquier comstackdor incluso se molesta en hacer cumplir la cláusula añadida de C, y etc. El objective 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 fonts excelentes sobre esto, y creo que tengo un resumen muy completo de eso. 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 si la situación cambia.

Esta es la primera vez que trato de resumir una situación muy compleja, que parece mal definida incluso para muchos arquitectos de idiomas, por lo que agradeceré las 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 por @tab, y aprecié mucho los enlaces contenidos (que iluminan, si no son concluyentes) los informes de defectos del GCC y el Grupo de Trabajo: respuesta por tabulación en StackOverflow

El enlace de GCC contiene una discusión interesante y revela una gran cantidad de confusión e interpretaciones contradictorias por parte del Comité y de los vendedores del comstackdor, en torno al tema de union struct miembros de la union , los juegos de palabras y los alias en C y 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, encontramos nuestro camino hacia el primero de los dos documentos fundamentales:

Origen de la línea agregada en C99

La propuesta C N685 es el origen de la cláusula añadida con respecto a la visibilidad de una statement de tipo de union . A través de lo que algunos afirman (ver subproceso GCC n. ° 2) es una interpretación errónea de la asignación de “secuencia inicial común”, N685 estaba destinado a permitir la relajación de las reglas de aliasing 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 statement 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 lo 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 y los comentarios del CCG a continuación como @ ecatmur, esta propuesta, que parece ordenar de forma especulativa el aliasing para cualquier tipo de struct que tenga alguna instancia dentro de alguna union visible para esta TU, parece haber recibido mucha burla y rara vez se implementó .

Es obvio lo difícil que sería satisfacer esta interpretación de la cláusula añadida sin paralizar totalmente muchas optimizaciones, con poco beneficio, ya que pocos codificadores querrían esta garantía, y aquellos que sí pueden simplemente activar fno-strict-aliasing (que IMO indica problemas más grandes). Si se implementa, es más probable que esta asignación atrape personas e interactúe de manera espuria 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 otro lugar, @Potatoswatter en esta respuesta aquí en SO declara que:

La parte de visibilidad se omitió intencionalmente en C ++ porque se considera ampliamente absurda e imposible de implementar.

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

La gente en esa discusión está esencialmente “en el registro” allí. Andrew Pinski es un tipo incondicional de back-end 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.

Potatoswatter, en el mismo subproceso 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 en el futuro se definirá específicamente, frente a cualquier otro indicador; compárelo con mi sección final a continuación sobre C. Actualmente, sin embargo, no es (y de nuevo, OMI, esto es bueno).

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

Entonces, con la línea nefasta de N685 … “dejar de lado” … volvemos a suponer que los punteros en la secuencia inicial común no son especiales en términos de aliasing. Todavía. vale la pena confirmar lo que significa este párrafo en C ++ sin él. Bueno, el segundo hilo de GCC arriba enlaza con otra gem:

Defecto C ++ 1719 . Esta propuesta ha alcanzado el estado de DRWP : “Un problema de DR cuya resolución se refleja en el documento de trabajo actual. El documento de trabajo es un borrador de una versión futura del estándar” – cite . Esto es post 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 , como sigue. Estoy negando 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 {“activo” indica una instancia de union , no solo el tipo} (9.5 [clase.unión]) de estructura tipo T1 , se permite leer {anteriormente “inspeccionar”} un no- elemento de datos estáticos m de otro miembro de unión de tipo de 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 glvalue no volátil tiene un comportamiento indefinido (7.1.6.1 [dcl.type.cv]). -Finalizar nota]

Esto parece aclarar el significado de la antigua redacción: para mí, dice que cualquier “interpelación” específicamente permitida entre union struct miembro de la 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, punteros a ellas pasadas 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 yo. Oye, hablando de eso, mira abajo!

El resultado es que, como demostraron muy bien en @ecatmur y en los tickets de GCC, esto deja union struct miembro de la union por definición en C ++, y prácticamente en C, sujeto a las mismas reglas estrictas de aliasing que cualquier otro 2 punteros oficialmente no relacionados. La garantía explícita de poder leer la secuencia inicial común de union struct miembros de la union inactiva ahora está más claramente definida, sin incluir la “visibilidad” vaga e inimaginablemente tediosa de aplicar como lo intentó N685 para C. Con esta definición, la principal los comstackdores se han estado comportando según lo previsto para C ++. En cuanto a C?

Posible reversió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 lenguaje excelente:

Martin Sebor 2015-04-27 14:57:16 UTC Si uno de ustedes puede explicar el problema con él, estoy dispuesto a escribir un documento y enviarlo al WG14 y solicitar que se modifique 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 mejorar las partes de aliasing 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 es también un problema pendiente que valdría la pena revisar y corregir el WG14 “.

Potatoswatter inspirantemente concluye:

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 posteriores 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 fuera de la unión. Es decir, supongamos que tenemos esto:

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

Ahora supongamos que en alguna función tenemos un puntero struct s1 *p1 que sabemos que fue eliminado del miembro m1 de dicha unión. Podemos convertir esto en un puntero struct s2 * y aún acceder a los miembros que están en común con struct s1 . Pero en algún lugar del scope, una statement de union u debe ser visible. Y tiene que ser la statement completa, que informa al comstackdor que los miembros son struct s1 y struct s2 .

El bash probable es que si existe tal tipo de ámbito, el comstackdor tenga conocimiento de 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 acceda a una 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 aliasing estricto.

Dado que la redacción está ausente de C ++, para aprovechar la regla de “relajación inicial común de miembros” en ese idioma, 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