Propósito de las uniones en C y C ++

254

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.

legends2k
fuente
11
En pocas palabras, los compiladores pueden insertar relleno entre elementos en una estructura. Por lo tanto, b, g, r,y apueden no ser contiguos, por lo que no coincida con el diseño de un uint32_t. Esto se suma a los problemas de Endianess que otros han señalado.
Thomas Matthews
8
Esto es exactamente por qué no debe etiquetar las preguntas C y C ++. Las respuestas son diferentes, pero dado que los respondedores ni siquiera dicen qué etiqueta están respondiendo (¿lo saben?), Se obtiene basura.
Pascal Cuoq
55
@downvoter Gracias por no explicar, entiendo que quieres que comprenda mágicamente tu queja y no la repita en el futuro: P
legends2k
1
Con respecto a la intención original de tener unión , tenga en cuenta que la norma C es posterior a las uniones por varios años. Un vistazo rápido a Unix V7 muestra algunas conversiones de tipos a través de uniones.
ninjalj
3
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 "
underscore_d

Respuestas:

409

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") no es un uso válido de los sindicatos. Generalmente conduce a un comportamiento indefinido que se 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.

Hormiga
fuente
37
¡+1 por ser elaborado, dar un ejemplo práctico simple y decir sobre el legado de los sindicatos!
legends2k
66
El problema que tengo con esta respuesta es que la mayoría de los sistemas operativos que he visto tienen archivos de encabezado que hacen exactamente esto. Por ejemplo, lo he visto en versiones antiguas (anteriores a 64 bits) de <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.
TED
31
@AndreyT "Nunca ha sido legal utilizar sindicatos para la tipología hasta hace muy poco": 2004 no es "muy reciente", especialmente teniendo en cuenta que es solo el C99 el que inicialmente fue redactado torpemente, al parecer hacer que la tipificación a través de los sindicatos sea indefinida. En realidad, el tipo de letra aunque los sindicatos es legal en C89, legal en C11, y fue legal en C99 todo el tiempo, aunque tomó hasta 2004 para que el comité corrigiera la redacción incorrecta y la posterior publicación de TC3. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
Pascal Cuoq
66
@ legends2k El lenguaje de programación se define de forma estándar. El Corrigendum técnico 3 del estándar C99 permite explícitamente la escritura en su nota al pie 82, que le invito a leer por sí mismo. Esta no es la televisión donde se entrevista a estrellas de rock y expresan sus opiniones sobre el cambio climático. La opinión de Stroustrup tiene cero influencia en lo que dice el estándar C.
Pascal Cuoq
66
@ legends2k " Sé que la opinión de cualquier individuo no importa y solo el estándar " La opinión de los escritores de compiladores es mucho más importante que la "especificación" del lenguaje (extremadamente pobre).
curioso
38

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;
Erich Kitzmueller
fuente
Estoy totalmente de acuerdo, sin entrar en el caos de comportamiento indefinido, tal vez este sea el comportamiento mejor intencionado de los sindicatos que se me ocurra; pero no es desperdiciar espacio cuando solo estoy usando, digamos into char*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?
legends2k
3
leyendas: en algunos casos, simplemente no puedes hacer eso. Utiliza algo como VAROBJECT en C en los mismos casos cuando usa Object en Java.
Erich Kitzmueller
La estructura de datos de los sindicatos etiquetados parece ser un uso legítimo de los sindicatos, como usted explica.
legends2k
También dé un ejemplo de cómo usar los valores.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1
@CiroSantilli 新疆 改造 中心 六四 事件 法轮功 Una parte de un ejemplo de C ++ Primer , podría ayudar. wandbox.org/permlink/cFSrXyG02vOSdBk2
Rick
34

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

David Rodríguez - dribeas
fuente
¡+1 por la respuesta clara y simple! Estoy de acuerdo, para la portabilidad, el método que ha dado en el segundo párrafo es válido; pero ¿puedo usar la forma que he planteado en la pregunta, si mi código está vinculado a una sola arquitectura (pagando el precio de la capacidad de prueba), ya que ahorra 4 bytes por cada valor de píxel y algo de tiempo ahorrado al ejecutar esa función ?
legends2k
El problema endian no obliga al estándar a declararlo como un comportamiento indefinido: reinterpret_cast tiene exactamente los mismos problemas endian, pero tiene un comportamiento definido de implementación.
JoeG
1
@ legends2k, el problema es que el optimizador puede suponer que un uint32_t no se modifica al escribir en un uint8_t y, por lo tanto, obtiene un valor incorrecto cuando el optimizado usa esa suposición ... @ Joe, el comportamiento indefinido aparece tan pronto como accede al puntero (lo sé, hay algunas excepciones).
Programador del
1
@ legends2k / AProgrammer: el resultado de un reinterpret_cast es la implementación definida. El uso del puntero devuelto no produce un comportamiento indefinido, solo un comportamiento definido de implementación En otras palabras, el comportamiento debe ser consistente y definido, pero no es portátil.
JoeG
1
@ legends2k: cualquier optimizador decente reconocerá las operaciones bit a bit que seleccionan un byte completo y generan código para leer / escribir el byte, igual que la unión pero bien definido (y portátil). por ejemplo, uint8_t getRed () const {return color & 0x000000FF; } void setRed (uint8_t r) {color = (color & ~ 0x000000FF) | r; }
Ben Voigt
22

El uso más comúnunion que encuentro regularmente es el alias .

Considera lo siguiente:

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

¿Qué hace esto? Permite el acceso limpio y ordenado de Vector3f vec;los miembros de a por cualquier 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 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.

bobobobo
fuente
3
Esto también se llama, type-punningque también se menciona en la pregunta. También el ejemplo en la pregunta muestra un ejemplo similar.
legends2k
44
No es un juego de palabras. En mi ejemplo los tipos coinciden , por lo que no hay "juego de palabras", es simplemente un alias.
bobobobo
3
Sí, pero aún así, desde un punto de vista absoluto del estándar del idioma, el miembro escrito y leído es diferente, lo cual no está definido como se menciona en la pregunta.
legends2k
3
Espero que un estándar futuro solucione este caso particular que se permitirá bajo la regla de "subsecuencia inicial común". Sin embargo, las matrices no participan en esa regla según la redacción actual.
Ben Voigt
3
@curiousguy: claramente no existe el requisito de que los miembros de la estructura se coloquen sin relleno arbitrario. Si el código prueba la ubicación del miembro de estructura o el tamaño de la estructura, el código debería funcionar si los accesos se realizan directamente a través de la unión, pero una lectura estricta de la Norma indicaría que tomar la dirección de una unión o miembro de estructura produce un puntero que no se puede usar como un puntero de su propio tipo, pero primero debe convertirse de nuevo en un puntero al tipo de cierre o un tipo de carácter. Cualquier compilador que funcione remotamente ampliará el lenguaje haciendo que más cosas funcionen que ...
supercat
10

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 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
¿Lo has usado así (como en la pregunta)? :)
legends2k
Es un poco pedante, pero no acepto los "registros de variantes". Es decir, estoy seguro de que lo tenían en mente, pero si eran una prioridad, ¿por qué no proporcionarlos? "Proporcionar el bloque de construcción porque también podría ser útil construir otras cosas", parece intuitivamente más probable. Especialmente teniendo en cuenta al menos uno más aplicación que fue probablemente en cuenta - de memoria mapeada registros de E / S, donde los registros de entrada y salida (mientras solapada) son entidades distintas con sus propios nombres, tipos etc.
Steve314
@ Stev314 Si ese fuera el uso que tenían en mente, podrían haber hecho que no fuera un comportamiento indefinido.
@Neil: +1 para el primero en decir sobre el uso real sin golpear el comportamiento indefinido. Supongo que podrían haberlo hecho una implementación definida como otro tipo de operaciones de punteo (reinterpret_cast, etc.). Pero como pregunté, ¿lo has usado para escribir letras?
legends2k
@Neil: el ejemplo de registro mapeado en memoria no está indefinido, el endian / etc habitual a un lado y con una bandera "volátil". Escribir en una dirección en este modelo no hace referencia al mismo registro que leer la misma dirección. Por lo tanto, no hay un problema de "qué estás leyendo", ya que no estás leyendo, cualquiera que sea el resultado que escribiste en esa dirección, cuando lees solo estás leyendo una entrada independiente. El único problema es asegurarse de leer el lado de entrada de la unión y escribir el lado de salida. Era común en cosas incrustadas, probablemente todavía lo es.
Steve314
8

En C fue una buena manera 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.

Totonga
fuente
Aunque ambos ejemplos están perfectamente definidos en el estándar; pero, oye, usar campos de bits es seguro disparó código no portable, ¿no?
legends2k
No, no lo es. Por lo que sé, es ampliamente compatible.
Totonga
1
El soporte del compilador no se traduce en portátil. El Libro C : C (por lo tanto, C ++) no garantiza el orden de los campos dentro de las palabras de máquina, por lo que si los usa por la última razón, su programa no solo no será portátil, sino que también dependerá del compilador.
legends2k
5

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.

Paul R
fuente
2
¿No hay un problema endian? Una solución relativamente fácil en comparación con "indefinido", pero vale la pena tener en cuenta para algunos proyectos si es así.
Steve314
5

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 + unionconstrucción (pila asignada también, etc.) pero utiliza una lista de tipos de plantillas en lugar de enum:)

Matthieu M.
fuente
5

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

Mella
fuente
4

Técnicamente no está definido, pero en realidad la mayoría de los compiladores (¿todos?) Lo tratan exactamente igual que el uso reinterpret_castde un tipo a otro, cuyo resultado es la implementación definida. No perdería el sueño por tu código actual.

JoeG
fuente
" un reinterpret_cast de un tipo a otro, cuyo resultado es la implementación definida " . No, no lo es. Las implementaciones no tienen que definirlo, y la mayoría no lo define. Además, ¿cuál sería el comportamiento definido de implementación permitida de lanzar algún valor aleatorio a un puntero?
curioso
4

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.

Cubbi
fuente
4

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

philcolbourn
fuente
¿Un byte que tiene que estar alineado con palabras? Eso no tiene sentido. Un byte no tiene requisitos de alineación, por definición.
curioso
Sí, probablemente debería haber usado un mejor ejemplo. Gracias.
philcolbourn
@curiousguy: Hay muchos casos en los que uno puede desear que las matrices de bytes estén alineadas con palabras. Si uno tiene muchas matrices de, por ejemplo, 1024 bytes y con frecuencia desea copiar una a otra, tener alineadas las palabras puede en muchos sistemas duplicar la velocidad de una memcpy()de una a otra. Algunos sistemas pueden alinear especulativamente las char[]asignaciones que ocurren fuera de las estructuras / uniones por esa y otras razones. En el ejemplo existente, la suposición que ise superpondrá a todos los elementos de c[]no es portátil, pero eso es porque no hay garantía de eso sizeof(int)==4.
supercat
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.

Super gato
fuente
Aprecie la lección de historia, sin embargo, con el estándar que define tal y como indefinido, que no era el caso en la era C pasada donde el libro de K&R era el único "estándar", uno debe asegurarse de no usarlo para cualquier propósito y entrar en la tierra de la UB.
legends2k
2
@ legends2k: Cuando se escribió el Estándar, la mayoría de las implementaciones de C trataron a los sindicatos de la misma manera, y dicho tratamiento fue útil. Algunos, sin embargo, no lo hicieron, y los autores de la Norma se mostraron reacios a calificar cualquier implementación existente como "no conforme". En cambio, pensaron que si los implementadores no necesitaran el Estándar para decirles que hicieran algo (como lo demuestra el hecho de que ya lo estaban haciendo ), dejarlo sin especificar o indefinido simplemente preservaría el status quo . La noción de que debería hacer las cosas menos definidas de lo que estaban antes de que se escribiera el Estándar ...
supercat
2
... parece una innovación mucho más reciente. Lo que es particularmente triste de todo esto es que si los escritores de compiladores que apuntan a aplicaciones de alta gama descubrieran cómo agregar directivas de optimización útiles al lenguaje que la mayoría de los compiladores implementaron en la década de 1990, en lugar de destripar características y garantías que habían sido respaldadas por "solo "El 90% de las implementaciones, el resultado sería un lenguaje que podría funcionar mejor y de manera más confiable que el hipermoderno C.
supercat
2

Puede usar una unión por dos razones principales:

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

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.

Señor chico
fuente
2

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 puro enum(Rust puede contener datos adicionales en variantes de enumeración). Aquí hay un ejemplo de C ++:

template <typename T, typename E> struct Result {
    public:
    enum class Success : uint8_t { Ok, Err };
    Result(T val) {
        m_success = Success::Ok;
        m_value.ok = val;
    }
    Result(E val) {
        m_success = Success::Err;
        m_value.err = val;
    }
    inline bool operator==(const Result& other) {
        return other.m_success == this->m_success;
    }
    inline bool operator!=(const Result& other) {
        return other.m_success != this->m_success;
    }
    inline T expect(const char* errorMsg) {
        if (m_success == Success::Err) throw errorMsg;
        else return m_value.ok;
    }
    inline bool is_ok() {
        return m_success == Success::Ok;
    }
    inline bool is_err() {
        return m_success == Success::Err;
    }
    inline const T* ok() {
        if (is_ok()) return m_value.ok;
        else return nullptr;
    }
    inline const T* err() {
        if (is_err()) return m_value.err;
        else return nullptr;
    }

    // Other methods from https://doc.rust-lang.org/std/result/enum.Result.html

    private:
    Success m_success;
    union _val_t { T ok; E err; } m_value;
}
Kotauskas
fuente