Propósito de los sindicatos en C y C++


He usado uniones 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

Es en realidad un comportamiento indefinido, es decir, leer de un miembro de la unión que no sea el escrito recientemente conduce a un comportamiento indefinido. Si este no es el uso previsto de los sindicatos, ¿qué es? ¿Puede alguien por favor explicarlo elaboradamente?

Actualización:

Quería aclarar algunas cosas en retrospectivo.

  • La respuesta a la pregunta no es la misma para C y C++; mi ignorante yo más joven lo etiquetó como C y C++.
  • Después de revisar el estándar de C++11, no podría decir de manera concluyente que llama a acceder/inspeccionar a un miembro del sindicato inactivo es 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 object of this standard-layout union type contains one of the standard-layout structs, it is permitted to inspect the common initial sequence of any of standard-layout struct members. §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 miembros iniciales.

  • Mientras que en C, (C99 TC3-DR 283 en adelante) es legal hacerlo ( gracias a Pascal Cuoq por traer esto a colación). Sin embargo, intentar hacer todavía puede conducir a un comportamiento indefinido, si el valor leído resulta ser inválido (lo que se llama "representación trap") para el tipo a través del cual se lee. De lo contrario, el valor leído está definido por la implementación.
  • C89/90 lo llamó bajo comportamiento no especificado (Anexo J) y el libro de K&R dice que está definida la implementación. Cita de K & R:

    Este es el propósito de una unión - una sola variable que puede legítimamente tener cualquiera de 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 realizar 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 del TC++PL de Stroustrup (énfasis mine)

    El uso de uniones puede ser esencial para la compatness de los datos [...] a veces se usa mal para "conversión de tipos".

Sobre todo, esta pregunta (cuyo título permanece sin cambios desde mi pregunta) se planteó con la intención de comprender el propósito de las uniones Y no en lo que el estándar permite Por ejemplo, Usar la herencia para reutilizar el código está, por supuesto, permitido por el estándar C++, pero no era el propósito o la intención original de presentamos 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.

Author: Community, 2010-02-22

14 answers

El propósito de los sindicatos es bastante obvio, pero por alguna razón la gente lo echa de menos con bastante frecuencia.

El propósito de union es guardar memoria usando la misma región de memoria para almacenar diferentes objetos en diferentes momentos. Eso es todo.

Es como una habitación en un hotel. Diferentes personas viven en él durante períodos de tiempo que no se superponen. Estas personas nunca se conocen, y generalmente no saben nada el uno del otro. Gestionando adecuadamente el tiempo compartido de las habitaciones (es decir, asegurándose de que diferentes 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 para lo que están los hoteles.

Eso es exactamente lo que hace la unión. Si sabe que varios objetos en su programa contienen valores con vidas no superpuestas, 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 de tiempo, un sindicato tiene como máximo 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 memoria (también conocido como "tipo de juego de palabras") no es un uso válido de uniones. En general conduce a un comportamiento indefinido se describe como producir un comportamiento definido por la implantación en C89/90.

EDITAR: El uso de uniones para propósitos de juego de tipo (es decir, escribir un miembro y luego leer otro) recibió una definición más detallada en una de las Correcciones Técnicas al estándar C99 (ver DR#257y DR#283). Sin embargo, tenga en cuenta que formalmente esto no lo protege de encontrarse con un comportamiento indefinido al intentar leer una trampa representatividad.

 304
Author: AnT,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-22 01:31:30

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:

struct VAROBJECT
{
    enum o_t { Int, Double, String } objectType;

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;
 33
Author: Erich Kitzmueller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-22 11:24:30

El comportamiento es indefinido 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 endianness. 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 usaran la misma endianidad (y restricciones de alineación de memoria)...) limitar el uso.

Si estás usando C++ (estás usando dos etiquetas) y realmente te importa acerca de la portabilidad, entonces solo puede usar la estructura y proporcionar un setter que tome uint32_t y establezca los campos apropiadamente a través de operaciones de máscara de bits. Lo mismo se puede hacer en C con una función.

Edit : Esperaba que un programador escribiera una respuesta para votar y cerrara esta. Como han señalado algunos comentarios, la endianidad se trata en otras partes del estándar al permitir que cada implementación decida qué hacer, y también se puede manejar la alineación y el relleno diferente. Ahora, las estrictas reglas de alias a las que se refiere implícitamente AProgrammer son un punto importante aquí. El compilador puede hacer suposiciones sobre la modificación (o falta de modificación) de 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.

 33
Author: David Rodríguez - dribeas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-22 22:46:21

El uso más común deunion que encuentro regularmente es aliasing .

Considere lo siguiente:

union Vector3f
{
  struct{ float x,y,z ; } ;
  float elts[3];
}

, ¿Qué hace esto? Permite un acceso limpio y ordenado de los miembros de un Vector3f vec; por ya sea nombre:

vec.x=vec.y=vec.z=1.f ;

O por acceso entero a 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 puedes hacer. En otros casos, especialmente cuando el eje se elige programáticamente, lo más fácil de hacer es acceder a la eje por índice numérico-0 para x, 1 para y, y 2 para z.

 12
Author: bobobobo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-11 22:43:16

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 la variante realmente contiene. 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.

 9
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-22 11:22:20

En C era una buena manera de implementar algo así 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 está usando menos memoria que una estructura que tiene todo el miembro.

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 bits.

 7
Author: Totonga,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-22 12:10:35

En C++, Boost Variant implementa una versión segura de la unión, diseñada para evitar un comportamiento indefinido tanto como sea posible.

Sus actuaciones son idénticas a la construcción enum + union (stack allocated too etc) pero utiliza una lista de tipos de plantilla en lugar de la enum :)

 5
Author: Matthieu M.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-22 13:09:57

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. Ciertamente, es preferible al juego de tipo, que bien puede generar código roto con algunos compiladores.

 4
Author: Paul R,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-22 11:22:40

Técnicamente es indefinido, pero en realidad la mayoría (¿todos?) los compiladores lo tratan exactamente igual que usando un 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.

 4
Author: Joe Gauterin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-22 11:49:28

Para un ejemplo más del uso real de uniones, el marco CORBA serializa objetos usando el enfoque 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.

 4
Author: Cubbi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-22 19:48:37

El comportamiento puede ser indefinido, pero eso solo 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, las uniones no son solo para ahorrar espacio. Pueden ayudar a los compiladores modernos con el tipo de juego de palabras. Si reinterpret_cast<> todo lo que el compilador no puede hacer suposiciones acerca de lo que está haciendo. Puede que tenga que lanzar lejos lo que sabe acerca de su tipo y empezar de nuevo (forzando una escritura de nuevo a la memoria, que es muy ineficiente en estos días en comparación con la velocidad de reloj de la CPU).

 3
Author: Nick,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-01-18 11:28:43

Otros han mencionado las diferencias arquitectónicas (little - big endian).

Leí el problema de que como la memoria para las variables es compartida, entonces al escribir a una, las otras cambian y, dependiendo de su tipo, el valor podría no tener sentido.

Eg. Union{ flotador f; int i; } x;

Escribir a x. no tendría sentido si luego lees de x. f-a menos que eso sea lo que pretendes para mirar el signo, exponente o mantisa componentes del flotador.

Creo que también hay un problema de alineación: Si algunas variables deben estar alineadas con palabras, es posible que no obtenga el resultado esperado.

Eg. Union{ char c[4]; int i; } x;

Si, hipotéticamente, en alguna máquina un char tuviera que estar alineado con palabras, entonces c[0] y c[1] compartirían almacenamiento con i pero no con c[2] y c[3].

 3
Author: philcolbourn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-11 11:30:50

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->member" fue definido como desplazamiento del miembro a " ptr " y acceder a la dirección resultante utilizando el tipo de miembro. Este diseño hizo posible utilizar el mismo ptr con miembro nombres tomados de diferentes definiciones de estructura pero con el mismo desplazamiento; los programadores usaron esa habilidad para una variedad de propósitos.

Cuando los miembros de la estructura eran asignado sus propios espacios de nombres, se hizo imposible declarar dos miembros de la estructura con el mismo desplazamiento. Añadir uniones a el lenguaje hizo posible lograr la misma semántica que había sido disponible en versiones anteriores del lenguaje (aunque la incapacidad de tener los nombres exportados a un contexto de encierro pueden haber necesitado aún el uso de un buscar/reemplazar para reemplazar foo->miembro en foo->type1.miembro). Lo que fue importante no era tanto que las personas que agregaron sindicatos tienen alguna particular objetivo de uso en mente, sino más bien que proporcionan un medio por el cual los programadores que se había basado en la semántica anterior, para cualquier propósito , todavía debería ser capaz de lograr la misma semántica, incluso si tenían que utilizar un diferente sintaxis para hacerlo.

 3
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-21 16:24:32

Puede usar a una unión por dos razones principales:

  1. Una forma práctica de acceder a los mismos datos de diferentes maneras, como en tu ejemplo
  2. Una forma de ahorrar espacio cuando hay diferentes miembros de datos de los cuales solo uno puede estar 'activo'

1 Es realmente más bien un hack estilo C para acortar la escritura de código sobre la base de que sabes cómo funciona la arquitectura de memoria del sistema de destino. Como ya se ha dicho, normalmente puede salirse con la suya si en realidad no se dirige a un montón de diferentes plataformas. Creo que algunos compiladores podrían permitirle usar directivas de embalaje también (sé que lo hacen en estructuras)?

Un buen ejemplo de 2. se puede encontrar en la variante tipo utilizado ampliamente en COM.

 2
Author: Mr. Boy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-22 11:46:30