¿Acceso a un miembro sindical inactivo y comportamiento indefinido?

129

Tenía la impresión de que acceder a un unionmiembro que no sea el último conjunto es UB, pero parece que no puedo encontrar una referencia sólida (aparte de las respuestas que afirman que es UB pero sin ningún apoyo del estándar).

Entonces, ¿es un comportamiento indefinido?

Luchian Grigore
fuente
3
C99 (y creo que C ++ 11 también) permite explícitamente la escritura de tipos con sindicatos. Por lo tanto, creo que se trata del comportamiento de "implementación definida".
Mysticial
1
Lo he usado en varias ocasiones para convertir de int individual a char. Entonces, definitivamente sé que no está indefinido. Lo usé en el compilador Sun CC. Por lo tanto, aún podría ser dependiente del compilador.
go4sri
42
@ go4sri: Claramente, no sabes lo que significa que el comportamiento no esté definido. El hecho de que parecía funcionar para usted en algún caso no contradice su falta de definición.
Benjamin Lindley
44
Relacionado: Propósito de las Uniones en C y C ++
legends2k
44
@Mysticial, la publicación de blog a la que enlaza está muy específicamente relacionada con C99; esta pregunta está etiquetada solo para C ++.
davmac

Respuestas:

131

La confusión es que C permite explícitamente la escritura de tipos mediante una unión, mientras que C ++ () no tiene dicho permiso.

6.5.2.3 Estructura y miembros del sindicato

95) Si el miembro utilizado para leer el contenido de un objeto de unión no es el mismo que el miembro utilizado por última vez para almacenar un valor en el objeto, la parte apropiada de la representación del objeto del valor se reinterpreta como una representación de objeto en el nuevo escriba como se describe en 6.2.6 (un proceso a veces llamado "punzonado de tipo"). Esto podría ser una representación trampa.

La situación con C ++:

9.5 Uniones [class.union]

En una unión, como máximo uno de los miembros de datos no estáticos puede estar activo en cualquier momento, es decir, el valor de como máximo uno de los miembros de datos no estáticos puede almacenarse en una unión en cualquier momento.

C ++ más tarde tiene lenguaje que permite el uso de uniones que contienen structs con secuencias iniciales comunes; Sin embargo, esto no permite la escritura de tipos.

Para determinar si la unión de tipos está permitida en C ++, tenemos que buscar más. Recordar que es una referencia normativa para C ++ 11 (y C99 tiene un lenguaje similar al C11 que permite la unión de tipos):

3.9 Tipos [tipos.básicos]

4 - La representación de objeto de un objeto de tipo T es la secuencia de N objetos de caracteres sin signo tomados por el objeto de tipo T, donde N es igual a sizeof (T). La representación del valor de un objeto es el conjunto de bits que contienen el valor del tipo T. Para los tipos que se pueden copiar trivialmente, la representación del valor es un conjunto de bits en la representación del objeto que determina un valor, que es un elemento discreto de una implementación. conjunto de valores definido. 42
42) La intención es que el modelo de memoria de C ++ sea compatible con el del lenguaje de programación ISO / IEC 9899 C.

Se pone particularmente interesante cuando leemos

3.8 Vida útil del objeto [basic.life]

La vida útil de un objeto de tipo T comienza cuando: - se obtiene el almacenamiento con la alineación y el tamaño adecuados para el tipo T, y - si el objeto tiene una inicialización no trivial, su inicialización está completa.

Entonces, para un tipo primitivo (que ipso facto tiene una inicialización trivial) contenido en una unión, la vida útil del objeto abarca al menos la vida útil de la unión misma. Esto nos permite invocar

3.9.2 Tipos de compuestos [basic.compound]

Si un objeto de tipo T se encuentra en una dirección A, se dice que un puntero de tipo cv T * cuyo valor es la dirección A apunta a ese objeto, independientemente de cómo se obtuvo el valor.

Suponiendo que la operación en la que estamos interesados ​​es la escritura de tipos, es decir, tomar el valor de un miembro de la unión no activo, y dado lo anterior que tenemos una referencia válida al objeto al que hace referencia ese miembro, esa operación es lvalue-to -conversión de valor:

4.1 Conversión Lvalue-to-rvalue [conv.lval]

Un valor de gl de un tipo sin función, sin matriz Tse puede convertir en un valor pr. Si Tes un tipo incompleto, un programa que necesita esta conversión está mal formado. Si el objeto al que se refiere el valor gl no es un objeto de tipo Ty no es un objeto de un tipo derivado de T, o si el objeto no está inicializado, un programa que necesita esta conversión tiene un comportamiento indefinido.

La pregunta entonces es si un objeto que es un miembro de la unión no activo se inicializa por almacenamiento en el miembro de la unión activa. Por lo que puedo decir, este no es el caso y, aunque si:

  • una unión se copia en el charalmacenamiento de matriz y viceversa (3.9: 2), o
  • una unión se copia por byte en otra unión del mismo tipo (3.9: 3), o
  • Se accede a una unión a través de los límites del idioma mediante un elemento del programa conforme a ISO / IEC 9899 (hasta donde se define) (3.9: 4 nota 42), luego

el acceso a una unión por un miembro no activo se define y se define para seguir la representación del objeto y valor, el acceso sin una de las interposiciones anteriores es un comportamiento indefinido. Esto tiene implicaciones para las optimizaciones permitidas en dicho programa, ya que la implementación puede, por supuesto, asumir que no ocurre un comportamiento indefinido.

Es decir, aunque podemos formar legítimamente un valor l para un miembro sindical no activo (razón por la cual está bien asignar a un miembro no activo sin construcción) se considera no inicializado.

ecatmur
fuente
55
3.8 / 1 dice que la vida útil de un objeto termina cuando se reutiliza su almacenamiento. Eso me indica que un miembro no activo de la vida de un sindicato ha finalizado porque su almacenamiento se ha reutilizado para el miembro activo. Eso significaría que está limitado en cómo usa el miembro (3.8 / 6).
bames53
2
Según esa interpretación, cada bit de memoria contiene simultáneamente objetos de todos los tipos que son trivialmente iniciables y tienen una alineación apropiada ... Entonces, la vida útil de cualquier tipo no trivialmente iniciable termina inmediatamente ya que su almacenamiento se reutiliza para todos estos otros tipos ( y no reiniciar porque no son trivialmente iniciables)
bames53
3
La redacción 4.1 está completamente y completamente rota y desde entonces ha sido reescrita. No permitía todo tipo de cosas perfectamente válidas: no permitía memcpyimplementaciones personalizadas (acceder a objetos usando unsigned charvalores), no permitía acceder a *pdespués int *p = 0; const int *const *pp = &p;(aunque la conversión implícita de int**a const int*const*es válida), no permitía incluso acceder cdespués struct S s; const S &c = s;. CWG número 616 . ¿La nueva redacción lo permite? También hay [basic.lval].
2
@Omnifarious: Eso tendría sentido, aunque también necesitaría aclarar (y el Estándar C también necesita aclarar, por cierto) lo que el &operador unario quiere decir cuando se aplica a un miembro del sindicato. Creo que el puntero resultante debería poder usarse para acceder al miembro al menos hasta la próxima vez que se use el siguiente uso directo o indirecto de cualquier otro valor de miembro, pero en gcc el puntero no se puede usar ni tanto tiempo, lo que plantea una pregunta de qué &se supone que el operador quiere decir.
supercat
44
Una pregunta con respecto a "Recordemos que c99 es una referencia normativa para C ++ 11" ¿No es eso solo relevante, cuando el estándar c ++ se refiere explícitamente al estándar C (por ejemplo, para las funciones de la biblioteca c)?
MikeMB
28

El estándar C ++ 11 lo dice de esta manera

9.5 Uniones

En una unión, como máximo uno de los miembros de datos no estáticos puede estar activo en cualquier momento, es decir, el valor de como máximo uno de los miembros de datos no estáticos puede almacenarse en una unión en cualquier momento.

Si solo se almacena un valor, ¿cómo puede leer otro? Simplemente no está ahí.


La documentación de gcc enumera esto en Comportamiento definido de implementación

  • Se accede a un miembro de un objeto de unión utilizando un miembro de un tipo diferente (C90 6.3.2.3).

Los bytes relevantes de la representación del objeto se tratan como un objeto del tipo utilizado para el acceso. Ver Type-punning. Esto puede ser una representación trampa.

indicando que esto no es requerido por el estándar C.


2016-01-05: A través de los comentarios, me vinculé al Informe de defectos C99 # 283 que agrega un texto similar como una nota al pie del documento estándar C:

78a) Si el miembro utilizado para acceder al contenido de un objeto de unión no es el mismo que el miembro utilizado por última vez para almacenar un valor en el objeto, la parte apropiada de la representación del objeto del valor se reinterpreta como una representación de objeto en el nuevo escriba como se describe en 6.2.6 (un proceso a veces llamado "punzonamiento de tipo"). Esto podría ser una representación trampa.

Sin embargo, no estoy seguro si aclara mucho, considerando que una nota al pie no es normativa para el estándar.

Bo Persson
fuente
10
@LuchianGrigore: UB no es lo que el estándar dice que es UB, sino que es lo que el estándar no describe cómo debería funcionar. Este es exactamente tal caso. ¿La norma describe lo que sucede? ¿Dice que su implementación está definida? No y no. Entonces es UB. Además, con respecto al argumento de "los miembros comparten la misma dirección de memoria", deberá consultar las reglas de alias, que lo llevarán nuevamente a UB.
Yakov Galka
55
@Luchian: Está bastante claro lo que significa activo, "es decir, el valor de como máximo uno de los miembros de datos no estáticos se puede almacenar en una unión en cualquier momento".
Benjamin Lindley
55
@LuchianGrigore: Sí, los hay. Hay una cantidad infinita de casos que el estándar no aborda (y no puede). (C ++ es una VM completa de Turing, por lo que está incompleta). ¿Y qué? Explica qué significa "activo", consulte la cita anterior, después de "eso es".
Yakov Galka
8
@LuchianGrigore: La omisión de la definición explícita de comportamiento también es un comportamiento indefinido no considerado, de acuerdo con la sección de definiciones.
jxh
55
@Claudiu Eso es UB por una razón diferente: viola el alias estricto.
Mysticial
18

Creo que lo más cercano al estándar es decir que su comportamiento indefinido es donde define el comportamiento para una unión que contiene una secuencia inicial común (C99, §6.5.2.3 / 5):

Se hace una garantía especial para simplificar el uso de las 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 las comunes. parte inicial 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.

C ++ 11 proporciona requisitos / permisos similares en §9.2 / 19:

Si una unión de diseño estándar contiene dos o más estructuras de diseño estándar que comparten una secuencia inicial común, y si el objeto de unión de diseño estándar actualmente contiene una de estas estructuras de diseño estándar, se permite inspeccionar la parte inicial común de cualquier de ellos. Dos estructuras de diseño estándar comparten una secuencia inicial común si los miembros correspondientes tienen tipos compatibles con el diseño y ninguno de los miembros es un campo de bits o ambos son campos de bits con el mismo ancho para una secuencia de uno o más miembros iniciales.

Aunque ninguno lo declara directamente, ambos tienen una fuerte implicación de que "inspeccionar" (leer) a un miembro está "permitido" solo si 1) es (parte de) el miembro escrito más recientemente, o 2) es parte de una inicial común secuencia.

Esa no es una declaración directa de que hacer lo contrario es un comportamiento indefinido, pero es lo más cercano a lo que sé.

Jerry Coffin
fuente
Para completar esto, necesita saber qué "tipos compatibles con el diseño" son para C ++, o "tipos compatibles" para C.
Michael Anderson
2
@MichaelAnderson: Sí y no. Debe tratar con ellos cuando / si desea estar seguro de si algo cae dentro de esta excepción, pero la verdadera pregunta aquí es si algo que claramente cae fuera de la excepción realmente le da UB. Creo que eso está lo suficientemente implícito aquí para aclarar la intención, pero no creo que se haya dicho directamente.
Jerry Coffin
Esta cosa de "secuencia inicial común" podría haber salvado 2 o 3 de mis proyectos del Rewrite Bin. Estaba furioso cuando leí por primera vez sobre la mayoría de los usos punzantes de los mensajes de texto unionindefinidos, ya que un blog en particular me había dado la impresión de que esto estaba bien, y construí varias estructuras y proyectos grandes a su alrededor. Ahora creo que podría estar bien después de todo, ya que mis unions contienen clases que tienen los mismos tipos en la parte delantera
subrayado_d
@JerryCoffin, creo que estaba insinuando la misma pregunta que yo: ¿qué pasa si nuestro unioncontiene, por ejemplo, a uint8_ty a class Something { uint8_t myByte; [...] };? Supongo que esta condición también se aplicaría aquí, pero está redactada muy deliberadamente para permitir solo structs. Afortunadamente, ya estoy usando esos en lugar de primitivas crudas: O
underscore_d
@underscore_d: El estándar C al menos cubre esa pregunta: "Un puntero a un objeto de estructura, convertido adecuadamente, apunta a su miembro inicial (o si ese miembro es un campo de bits, entonces a la unidad en la que reside) , y viceversa."
Jerry Coffin
12

Algo que aún no se menciona en las respuestas disponibles es la nota a pie de página 37 en el párrafo 21 de la sección 6.2.5:

Tenga en cuenta que el tipo agregado no incluye el tipo de unión porque un objeto con tipo de unión solo puede contener un miembro a la vez.

Este requisito parece implicar claramente que no debe escribir en un miembro y leer en otro. En este caso, podría ser un comportamiento indefinido por falta de especificación.

mpu
fuente
Muchas implementaciones documentan sus formatos de almacenamiento y reglas de diseño. Tal especificación implicaría en muchos casos cuál sería el efecto de leer el almacenamiento de un tipo y escribir como otro en ausencia de reglas que digan que los compiladores no tienen que usar su formato de almacenamiento definido, excepto cuando las cosas se leen y escriben usando punteros de un tipo de personaje.
supercat
-3

Bien explico esto con un ejemplo.
Supongamos que tenemos la siguiente unión:

union A{
   int x;
   short y[2];
};

Supongo que sizeof(int)da 4, y sizeof(short)da 2.
cuando escribes union A a = {10}bien, crea una nueva var de tipo A y pon el valor 10.

su memoria debería verse así: (recuerde que todos los miembros del sindicato obtienen la misma ubicación)

       El | x |
       El | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0000 0000 | 0000 0000 | 0000 1010 |
       -----------------------------------------

como puede ver, el valor de ax es 10, el valor de ay 1 es 10 y el valor de ay [0] es 0.

ahora, ¿qué pasaría si hago esto?

a.y[0] = 37;

nuestra memoria se verá así:

       El | x |
       El | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0010 0101 | 0000 0000 | 0000 1010 |
       -----------------------------------------

esto convertirá el valor de ax a 2424842 (en decimal).

ahora, si su unión tiene un flotante, o doble, su mapa de memoria puede ser más desordenado, debido a la forma en que almacena los números exactos. Más información puede obtener aquí .

elyashiv
fuente
18
:) Esto no es lo que pregunté. Sé lo que pasa internamente. Sé que funciona Le pregunté si está en el estándar.
Luchian Grigore