sindicales - Propósito de los sindicatos en C y C++
que es un sindicato (14)
He utilizado los sindicatos antes cómodamente; Hoy me alarmé cuando leí este post y llegué a saber 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, la lectura de un miembro de la unión que no sea la que se escribió recientemente conduce a un comportamiento indefinido. Si este no es el uso previsto de los sindicatos, ¿cuál es? ¿Alguien puede explicarlo detalladamente?
Actualizar:
Quería aclarar algunas cosas en retrospectiva.
- La respuesta a la pregunta no es la misma para C y C ++; mi yo más joven e ignorante lo etiquetó como C y C ++.
- Después de recorrer el estándar de C ++ 11, no puedo decir de manera concluyente que el acceso / inspección de un miembro de la unión no activo no esté definido / no especificado / esté 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, está permitido inspeccionar la secuencia inicial común de cualquier de miembros de la 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 dos 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 llevar a un comportamiento indefinido , si el valor leído no es válido (lo que se conoce como "representación de trampa") para el tipo en el que se lee. De lo contrario, el valor leído es la implementación definida.
C89 / 90 mencionó esto 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 sola variable que puede legítimamente ser titular de cualquiera 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 está almacenado 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 [...] que a veces se usa incorrectamente para "conversión de tipo ".
Por encima de todo, esta pregunta (cuyo título permanece sin cambios desde mi consulta) se planteó con la intención de comprender el propósito de las uniones y no de lo que permite el estándar. Por ejemplo, el uso de la herencia para la reutilización del código está permitido por el estándar C ++, pero no fue el propósito ni la intención original de introducir la herencia como una característica del lenguaje C ++ . Esta es la razón por la que la respuesta de Andrey sigue siendo la aceptada.
Aunque este es un comportamiento estrictamente indefinido, en la práctica funcionará con casi cualquier compilador. Es un paradigma tan utilizado que cualquier compilador que se precie tendrá que hacer "lo correcto" en casos como este. Ciertamente es preferible a la tipificación de tipos, que puede generar código roto con algunos compiladores.
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.
union A {
int i;
double d;
};
A a[10]; // records in "a" can be either ints or doubles
a[0].i = 42;
a[1].d = 1.23;
Por supuesto, también necesita algún tipo de discriminador para decir lo que realmente contiene 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 ni destructores.
El comportamiento no está definido desde el punto de vista del lenguaje. Tenga en cuenta que las diferentes plataformas pueden tener diferentes restricciones en la alineación de la memoria y el endianness. El código en una máquina big endian frente a little endian actualizará los valores en la estructura de manera diferente. Reparar el comportamiento en el lenguaje requeriría que todas las implementaciones usen la misma endianness (y restricciones de alineación de la memoria ...) que limitan el uso.
Si está utilizando C ++ (está usando dos etiquetas) y realmente le importa la portabilidad, entonces puede usar la estructura y proporcionar un uint32_t
que tome el uint32_t
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.
Edit : Esperaba que AProgrammer escribiera una respuesta para votar y cerrar esta. Como han señalado algunos comentarios, el endianness se trata en otras partes de la norma al permitir que cada implementación decida qué hacer, y la alineación y el relleno también pueden manejarse de manera diferente. Ahora, las reglas estrictas de alias a las que se refiere implícitamente AProgrammer son un punto importante aquí. El compilador tiene permitido 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.
El comportamiento puede ser indefinido, pero eso simplemente significa que no hay un "estándar". Todos los compiladores decentes ofrecen #pragmas para controlar el empaquetamiento y la alineación, pero pueden tener diferentes valores predeterminados. Los valores predeterminados también cambiarán dependiendo de 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 punning. Si reinterpret_cast<>
todo lo que el compilador no puede hacer suposiciones sobre lo que está haciendo. Puede que tenga que deshacerse de lo que sabe sobre su tipo y comenzar de nuevo (forzar una escritura en la memoria, lo cual es muy ineficiente en estos días en comparación con la velocidad del reloj de la CPU).
El propósito de los sindicatos es bastante obvio, pero por alguna razón las personas lo extrañan bastante a menudo.
El propósito de la unión es ahorrar memoria utilizando 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 por 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, asegurarse de que distintas personas no sean asignadas 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 que no se superponen, entonces puede "fusionar" estos objetos en una unión y así ahorrar memoria. Al igual que una habitación de hotel tiene a lo sumo un inquilino "activo" en cada momento, un sindicato tiene a lo sumo un miembro "activo" en cada momento 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 a 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 "tipificación de puntos") no es un uso válido de uniones. En general, conduce a que el comportamiento indefinido se describe como que produce un comportamiento definido por la implementación en C89 / 90.
EDITAR: El uso de uniones con el propósito de tipear punning (es decir, escribir un miembro y luego leer otro) recibió una definición más detallada en uno de los Corrigendums Técnicos para el estándar C99 (ver DR#257 y DR # 283 ). Sin embargo, tenga en cuenta que, formalmente, esto no lo protege de encontrarse con un comportamiento indefinido al intentar leer una representación de captura.
El uso más común de la union
me encuentro regularmente es el aliasing .
Considera lo siguiente:
union Vector3f
{
struct{ float x,y,z ; } ;
float elts[3];
}
¿Qué hace esto? Permite el acceso limpio y ordenado de un Vector3f vec;
miembros por nombre
vec.x=vec.y=vec.z=1.f ;
o por acceso entero en la matriz
for( int i = 0 ; i < 3 ; i++ )
vec.elts[i]=1.f;
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.
En C ++, Boost Variant implementa una versión segura de la unión, diseñada para evitar todo lo posible un comportamiento indefinido.
Sus actuaciones son idénticas a la construcción enum + union
(pila asignada también, etc.) pero utiliza una lista de tipos de plantillas en lugar de la enum
:)
En C fue una buena forma de implementar algo como una variante.
enum possibleTypes{
eInt,
eDouble,
eChar
}
struct Value{
union Value {
int iVal_;
double dval;
char cVal;
} value_;
possibleTypes discriminator_;
}
switch(val.discriminator_)
{
case eInt: val.value_.iVal_; break;
En tiempos de poca memoria, esta estructura usa menos memoria que una estructura que tiene todos los miembros.
Por cierto C proporciona
typedef struct {
unsigned int mantissa_low:32; //mantissa
unsigned int mantissa_high:20;
unsigned int exponent:11; //exponent
unsigned int sign:1;
} realVal;
para acceder a los valores de bit.
En el lenguaje C como se documentó en 1974, todos los miembros de la estructura compartían un espacio de nombres común, y el significado de "ptr-> miembro" se definió como la adición del desplazamiento del miembro a "ptr" y el acceso a la dirección resultante utilizando el tipo del miembro. Este diseño hizo posible utilizar el mismo ptr con nombres de miembros tomados de diferentes definiciones de estructura pero con el mismo desplazamiento; Los programadores usaban 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. La adición de uniones al idioma hizo posible lograr la misma semántica que había estado disponible en versiones anteriores del idioma (aunque la incapacidad de tener nombres exportados a un contexto adjunto podría haber requerido el uso de un buscador / reemplazo para reemplazar foo-> miembro en foo-> type1.member). Lo que era importante no era tanto que las personas que agregaron sindicatos tuvieran en mente algún uso objetivo específico, sino que proporcionaran 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 alcanzar el objetivo. La misma semántica, incluso si tuvieran que usar una sintaxis diferente para hacerlo.
Otros han mencionado las diferencias de arquitectura (little - big endian).
Leí el problema de que, dado que la memoria de las variables se comparte, al escribir en una, las otras cambian y, dependiendo de su tipo, el valor podría carecer de sentido.
p.ej. unión {float f; int i; } X;
Escribir a xi no tendría sentido si luego lees desde xf, a menos que sea lo que pretendías para observar los componentes signo, exponente o mantisa del flotador.
Creo que también hay un problema de alineación: si algunas variables deben estar alineadas por palabra, es posible que no obtenga el resultado esperado.
p.ej. sindicato {char c [4]; int i; } X;
Si, hipotéticamente, en alguna máquina un char tuviera que estar alineado con la palabra, entonces c [0] yc [1] compartirían el almacenamiento con i, pero no c [2] y c [3].
Para un ejemplo más del uso real de las uniones, el marco CORBA serializa los objetos utilizando el enfoque de la unión etiquetada. Todas las clases definidas por el usuario son miembros de una unión (enorme), y un identificador entero le dice al demarshaller cómo interpretar la unión.
Podría usar uniones para crear estructuras como la siguiente, que contiene un campo que nos dice qué componente de la unión se utiliza realmente:
struct VAROBJECT
{
enum o_t { Int, Double, String } objectType;
union
{
int intValue;
double dblValue;
char *strValue;
} value;
} object;
Puedes usar aa union por dos razones principales:
- Una forma práctica de acceder a los mismos datos de diferentes maneras, como en su ejemplo
- Una forma de ahorrar espacio cuando hay diferentes miembros de datos de los cuales solo uno puede estar "activo"
1 En realidad, se trata más bien de un hack de estilo C para atajar el código de escritura sobre la base de que sabes cómo funciona la arquitectura de memoria del sistema de destino. Como ya se dijo, normalmente puede salirse con la suya si no se dirige a muchas plataformas diferentes. Creo que algunos compiladores podrían permitirle usar también directivas de empaquetado (sé que lo hacen en estructuras).
Un buen ejemplo de 2. se puede encontrar en el tipo VARIANT utilizado ampliamente en COM.
Técnicamente no está definido, pero en realidad la mayoría de los compiladores lo tratan exactamente igual que usar un reinterpret_cast
de un tipo a otro, cuyo resultado está definido por la implementación. No perdería el sueño sobre tu código actual.