He usado los sindicatos antes cómodamente; hoy me alarmó cuando leí esta publicación y supe que este código
union ARGB
{
uint32_t colour;
struct componentsTag
{
uint8_t b;
uint8_t g;
uint8_t r;
uint8_t a;
} components;
} pixel;
pixel.colour = 0xff040201; // ARGB::colour is the active member from now on
// somewhere down the line, without any edit to pixel
if(pixel.components.a) // accessing the non-active member ARGB::components
en realidad es un comportamiento indefinido, es decir, leer de un miembro del sindicato diferente al que se escribió recientemente conduce a un comportamiento indefinido. Si este no es el uso previsto de los sindicatos, ¿qué es? ¿Puede alguien explicarlo detalladamente?
Actualizar:
Quería aclarar algunas cosas en retrospectiva.
- La respuesta a la pregunta no es la misma para C y C ++; mi joven ignorante lo etiquetó como C y C ++.
- Después de revisar el estándar de C ++ 11, no podría decir de manera concluyente que exija que el acceso / inspección de un miembro del sindicato no activo sea indefinido / no especificado / definido por la implementación. Todo lo que pude encontrar fue §9.5 / 1:
Si una unión de diseño estándar contiene varias estructuras de diseño estándar que comparten una secuencia inicial común, y si un objeto de este tipo de unión de diseño estándar contiene una de las estructuras de diseño estándar, se permite inspeccionar la secuencia inicial común de cualquier de miembros de estructura de diseño estándar. §9.2 / 19: 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 iniciales miembros.
- Mientras que en C, ( C99 TC3 - DR 283 en adelante) es legal hacerlo ( gracias a Pascal Cuoq por mencionar esto). Sin embargo, intentar hacerlo puede conducir a un comportamiento indefinido , si el valor leído no es válido (lo que se denomina "representación de trampa") para el tipo que se lee. De lo contrario, el valor leído es la implementación definida.
C89 / 90 lo llamó bajo un comportamiento no especificado (Anexo J) y el libro de K&R dice que su implementación está definida. Cita de K&R:
Este es el propósito de una unión, una variable única que puede contener legítimamente uno de varios tipos. [...] siempre que el uso sea consistente: el tipo recuperado debe ser el tipo almacenado más recientemente. Es responsabilidad del programador hacer un seguimiento de qué tipo se almacena actualmente en una unión; los resultados dependen de la implementación si algo se almacena como un tipo y se extrae como otro.
Extracto de TC ++ PL de Stroustrup (énfasis mío)
El uso de uniones puede ser esencial para la compatibilidad de los datos, a veces [...] mal utilizados para la "conversión de tipos ".
Sobre todo, esta pregunta (cuyo título permanece sin cambios desde mi solicitud) se planteó con la intención de comprender el propósito de las uniones Y no sobre lo que permite el estándar . No era el propósito o la intención original de introducir la herencia como una característica del lenguaje C ++ . Esta es la razón por la cual la respuesta de Andrey sigue siendo la aceptada.
fuente
b, g, r,
ya
pueden no ser contiguos, por lo que no coincida con el diseño de unuint32_t
. Esto se suma a los problemas de Endianess que otros han señalado.scouring C++11's standard I couldn't conclusively say that it calls out accessing/inspecting a non-active union member is undefined [...] All I could find was §9.5/1
...¿De Verdad? Usted cita una nota de excepción , no el punto principal justo al comienzo del párrafo : "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 se pueden almacenar en una unión en cualquier momento ". - y hasta p4: "En general, uno debe usar llamadas explícitas de destructores y colocar nuevos operadores para cambiar el miembro activo de un sindicato "Respuestas:
El propósito de los sindicatos es bastante obvio, pero por alguna razón la gente lo extraña con bastante frecuencia.
El propósito de la unión es ahorrar memoria usando la misma región de memoria para almacenar diferentes objetos en diferentes momentos. Eso es.
Es como una habitación en un hotel. Diferentes personas viven en él durante períodos de tiempo no superpuestos. Estas personas nunca se encuentran, y generalmente no saben nada el uno del otro. Al administrar adecuadamente el tiempo compartido de las habitaciones (es decir, al asegurarse de que no se asignen diferentes personas a una habitación al mismo tiempo), un hotel relativamente pequeño puede proporcionar alojamiento a un número relativamente grande de personas, que es lo que los hoteles son para.
Eso es exactamente lo que hace la unión. Si sabe que varios objetos en su programa contienen valores con tiempos de vida de valores no superpuestos, entonces puede "fusionar" estos objetos en una unión y así ahorrar memoria. Al igual que una habitación de hotel tiene como máximo un inquilino "activo" en cada momento del tiempo, un sindicato tiene como máximo un miembro "activo" en cada momento del tiempo del programa. Solo se puede leer el miembro "activo". Al escribir en otro miembro, cambia el estado "activo" a ese otro miembro.
Por alguna razón, este propósito original del sindicato se "anuló" con algo completamente diferente: escribir un miembro de un sindicato y luego inspeccionarlo a través de otro miembro. Este tipo de reinterpretación de la memoria (también conocido como "tipo punning")
noesun uso válido de los sindicatos.Generalmente conduce a un comportamiento indefinido quese describe como la producción de un comportamiento definido por la implementación en C89 / 90.EDITAR: El uso de uniones con el propósito de escribir letras (es decir, escribir a un miembro y luego leer a otro) recibió una definición más detallada en uno de los Corrigenda técnicos según el estándar C99 (ver DR # 257 y DR # 283 ). Sin embargo, tenga en cuenta que formalmente esto no lo protege de tener un comportamiento indefinido al intentar leer una representación de trampa.
fuente
<time.h>
Windows y Unix. Descartarlo como "no válido" e "indefinido" no es realmente suficiente si se me va a pedir que comprenda el código que funciona de esta manera exacta.Puede usar uniones para crear estructuras como la siguiente, que contiene un campo que nos dice qué componente de la unión se usa realmente:
fuente
int
ochar*
para 10 artículos de objeto []; en cuyo caso, ¿puedo declarar estructuras separadas para cada tipo de datos en lugar de VAROBJECT? ¿No reduciría el desorden y usaría menos espacio?El comportamiento no está definido desde el punto de vista del lenguaje. Tenga en cuenta que diferentes plataformas pueden tener diferentes restricciones en la alineación de la memoria y la resistencia. El código en una máquina Big Endian versus una pequeña Endian actualizará los valores en la estructura de manera diferente. Arreglar el comportamiento en el lenguaje requeriría que todas las implementaciones usen el mismo endianness (y restricciones de alineación de memoria ...) limitando el uso.
Si está utilizando C ++ (está utilizando dos etiquetas) y realmente le importa la portabilidad, entonces puede usar la estructura y proporcionar un
uint32_t
configurador que tome y establezca los campos adecuadamente a través de las operaciones de máscara de bits. Lo mismo se puede hacer en C con una función.Editar : esperaba que AProgrammer escribiera una respuesta para votar y cerrara esta. Como algunos comentarios han señalado, la endianidad se trata en otras partes del estándar al permitir que cada implementación decida qué hacer, y la alineación y el relleno también se pueden manejar de manera diferente. Ahora, las estrictas reglas de alias a las que AProgrammer hace referencia implícita son un punto importante aquí. El compilador puede hacer suposiciones sobre la modificación (o falta de modificación) de las variables. En el caso de la unión, el compilador podría reordenar las instrucciones y mover la lectura de cada componente de color sobre la escritura a la variable de color.
fuente
El uso más común
union
que encuentro regularmente es el alias .Considera lo siguiente:
¿Qué hace esto? Permite el acceso limpio y ordenado de
Vector3f vec;
los miembros de a por cualquier nombre:o por acceso entero a la matriz
En algunos casos, acceder por nombre es lo más claro que puede hacer. En otros casos, especialmente cuando el eje se elige mediante programación, lo más fácil es acceder al eje mediante un índice numérico: 0 para x, 1 para y y 2 para z.
fuente
type-punning
que también se menciona en la pregunta. También el ejemplo en la pregunta muestra un ejemplo similar.Como usted dice, este es un comportamiento estrictamente indefinido, aunque "funcionará" en muchas plataformas. La verdadera razón para usar uniones es crear registros de variantes.
Por supuesto, también necesita algún tipo de discriminador para decir qué contiene realmente la variante. Y tenga en cuenta que en C ++ las uniones no son muy útiles porque solo pueden contener tipos de POD, efectivamente aquellos sin constructores y destructores.
fuente
En C fue una buena manera de implementar algo como una variante.
En tiempos de poca memoria, esta estructura usa menos memoria que una estructura que tiene todos los miembros.
Por cierto, C proporciona
para acceder a los valores de bit.
fuente
Aunque este es un comportamiento estrictamente indefinido, en la práctica funcionará con casi cualquier compilador. Es un paradigma tan ampliamente utilizado que cualquier compilador que se precie tendrá que hacer "lo correcto" en casos como este. Sin duda, es preferible a la escritura tipográfica, que bien puede generar código roto con algunos compiladores.
fuente
En C ++, Boost Variant implementa una versión segura de la unión, diseñada para evitar el comportamiento indefinido tanto como sea posible.
Sus rendimientos son idénticos a la
enum + union
construcción (pila asignada también, etc.) pero utiliza una lista de tipos de plantillas en lugar deenum
:)fuente
El comportamiento puede ser indefinido, pero eso solo significa que no hay un "estándar". Todos los compiladores decentes ofrecen #pragmas para controlar el empaquetado y la alineación, pero pueden tener valores predeterminados diferentes. Los valores predeterminados también cambiarán según la configuración de optimización utilizada.
Además, los sindicatos no son solo para ahorrar espacio. Pueden ayudar a los compiladores modernos con el tipo de juego de palabras. Si
reinterpret_cast<>
todo, el compilador no puede hacer suposiciones sobre lo que está haciendo. Puede que tenga que desechar lo que sabe sobre su tipo y comenzar de nuevo (forzando una escritura de nuevo en la memoria, que es muy ineficiente en estos días en comparación con la velocidad del reloj de la CPU).fuente
Técnicamente no está definido, pero en realidad la mayoría de los compiladores (¿todos?) Lo tratan exactamente igual que el uso
reinterpret_cast
de un tipo a otro, cuyo resultado es la implementación definida. No perdería el sueño por tu código actual.fuente
Para un ejemplo más del uso real de las uniones, el marco CORBA serializa objetos utilizando el enfoque de unión etiquetado. Todas las clases definidas por el usuario son miembros de una unión (enorme), y un identificador de número entero le dice al demarshaller cómo interpretar la unión.
fuente
Otros han mencionado las diferencias de arquitectura (little - big endian).
Leí el problema de que, dado que la memoria para las variables se comparte, al escribir en una, las otras cambian y, según su tipo, el valor podría no tener sentido.
p.ej. unión {flotador f; int i; } X;
Escribir en xi no tendría sentido si luego lees desde xf, a menos que eso sea lo que pretendías para mirar los signos, exponentes o componentes de mantisa del flotador.
Creo que también hay un problema de alineación: si algunas variables deben estar alineadas por palabras, entonces es posible que no obtenga el resultado esperado.
p.ej. unión {char c [4]; int i; } X;
Si, hipotéticamente, en alguna máquina un carácter tuviera que estar alineado con palabras, entonces c [0] yc [1] compartirían almacenamiento con i pero no con c [2] y c [3].
fuente
memcpy()
de una a otra. Algunos sistemas pueden alinear especulativamente laschar[]
asignaciones que ocurren fuera de las estructuras / uniones por esa y otras razones. En el ejemplo existente, la suposición quei
se superpondrá a todos los elementos dec[]
no es portátil, pero eso es porque no hay garantía de esosizeof(int)==4
.En el lenguaje C como se documentó en 1974, todos los miembros de la estructura compartían un espacio de nombres común, y se definió el significado de "ptr-> member" como agregar el desplazamiento del miembro a "ptr" y acceder a la dirección resultante utilizando el tipo de miembro. Este diseño permitió utilizar el mismo ptr con nombres de miembros tomados de diferentes definiciones de estructura pero con el mismo desplazamiento; los programadores usaron esa habilidad para una variedad de propósitos.
Cuando a los miembros de la estructura se les asignaron sus propios espacios de nombres, se hizo imposible declarar dos miembros de la estructura con el mismo desplazamiento. Agregar uniones al lenguaje hizo posible lograr la misma semántica que había estado disponible en versiones anteriores del lenguaje (aunque la imposibilidad de exportar nombres a un contexto cerrado aún puede haber requerido el uso de buscar / reemplazar para reemplazar foo-> member en foo-> type1.member). Lo importante no era tanto que las personas que agregaron sindicatos tengan en mente un uso objetivo particular, sino que proporcionen un medio por el cual los programadores que habían confiado en la semántica anterior, para cualquier propósito , aún deberían poder lograr el misma semántica incluso si tuvieran que usar una sintaxis diferente para hacerlo.
fuente
Puede usar una unión por dos razones principales:
1 Realmente es más un truco de estilo C para atajar el código de escritura sobre la base de que sabe cómo funciona la arquitectura de memoria del sistema de destino. Como ya se dijo, normalmente puede salirse con la suya si en realidad no apunta a muchas plataformas diferentes. ¿Creo que algunos compiladores también podrían permitirle usar directivas de empaque (sé que lo hacen en estructuras)?
Un buen ejemplo de 2. se puede encontrar en el tipo VARIANT utilizado ampliamente en COM.
fuente
Como otros mencionaron, las uniones combinadas con enumeraciones y envueltas en estructuras pueden usarse para implementar uniones etiquetadas. Un uso práctico es implementar Rust
Result<T, E>
, que originalmente se implementa utilizando un puroenum
(Rust puede contener datos adicionales en variantes de enumeración). Aquí hay un ejemplo de C ++:fuente