Una union es una estructura de datos donde todos sus miembros comparten la misma dirección de memoria. A diferencia de un struct, donde cada miembro tiene su propio espacio de almacenamiento, en una union el tamaño total está determinado por el miembro más grande y sus requisitos de alineación. Esto permite que un mismo bloque de bytes se interprete de múltiples formas, lo cual es la base del type punning. Se utiliza principalmente para interpretar el mismo conjunto de bits de diferentes maneras, permitiendo realizar conversiones de tipos a bajo nivel o el parseo de datos brutos. Es una herramienta esencial cuando necesitas inspeccionar la representación binaria de un tipo (como ver los bits de un float como si fueran un uint32_t) o para implementar tagged unions (tipos suma) que representan múltiples tipos de datos en un solo contenedor. Si intentas acceder a un miembro distinto al que fue escrito recientemente, el valor obtenido es indeterminado, aunque el estándar C permite esta práctica para realizar conversiones de tipos de bajo nivel de forma segura. Si ignoras la alineación o no gestionas correctamente la lógica de qué miembro es válido, podrías corromper la lógica del programa o causar errores de segmentación.
#include <stdio.h>
#include <stdint.h>
#include <string.h>
typedef enum {
DATA_INT,
DATA_FLOAT
} DataType;
/* Implementación de una Tagged Union usando anonymous union [C11] */
typedef struct {
DataType type;
union {
int32_t i;
float f;
};
} TaggedValue;
/* Union para type punning: inspeccionar bits de un float */
typedef union {
float f;
uint32_t u;
} FloatBits;
int main(void) {
/* 1. Uso de Tagged Union para representar un valor dinámico */
TaggedValue val;
val.type = DATA_FLOAT;
val.f = 3.14159f; // Acceso directo gracias a la anonymous union [C11]
if (val.type == DATA_FLOAT) {
printf("Valor float: %f\n", val.f);
}
/* 2. Type Punning: Ver la representación hexadecimal de los bits del float */
FloatBits bits;
bits.f = 3.14159f;
/* Accedemos a la misma memoria como uint32_t para ver su patrón de bits */
printf("Bits de 3.14159 en hex: 0x%08X\n", bits.u);
/* 3. Parsing de un buffer de red (bytes brutos) */
/* Simulamos un paquete de red que contiene un float (4 bytes) y un int (4 bytes) */
uint8_t buffer[8] = {0x00, 0x00, 0x48, 0x40, 0x01, 0x00, 0x00, 0x00};
float network_float;
/* Usar memcpy es la forma estándar y segura de evitar problemas de alineación
y las reglas de strict aliasing al mapear bytes brutos a tipos complejos. */
memcpy(&network_float, buffer, sizeof(float));
printf("Float parseado del buffer: %f\n", network_float);
return 0;
}
En la estructura TaggedValue, el miembro type funciona como un discriminante. Al usar una union anónima [C11], no necesitamos escribir val.union_name.f, sino que accedemos a val.f directamente, simplificando el código. Sin embargo, la memoria para i y f es la misma; si cambiáramos val.i justo antes de imprimir val.f, el valor sería basura.
En el caso de FloatBits, estamos realizando type punning. Cuando asignamos un valor a bits.f, el procesador almacena los bits de la coma flotante. Al leer bits.u, no estamos convirtiendo el número (como lo haría un cast de (uint32_t)3.14), sino que estamos interpretando esos mismos bits como un entero. Es una operación de coste cero a nivel de CPU.
Finalmente, en el parseo del buffer, utilizamos memcpy. Aunque podríamos intentar usar un puntero para castear el buffer, eso violaría las reglas de strict aliasing del compilador y podría dar resultados erróneos si el buffer no está alineado en la dirección de memoria que el tipo float requiere. memcpy le dice al compilador que simplemente mueva los bytes, lo cual es seguro y permite optimizaciones modernas.
El error frecuente
Un error crítico es intentar realizar type punning mediante el casting de punteros, lo cual viola las reglas de strict aliasing.
/* ERROR: Comportamiento indefinido (Undefined Behavior) */ float f = 3.14f; int *p = (int *)&f; // Violación de strict aliasing int i = *p; // El compilador puede asumir que 'i' y 'f' no se solapan
Si compilas con -O3, el compilador puede optimizar este código de forma agresiva bajo la premisa de que un int* nunca apunta al mismo objeto que un float*. Esto puede resultar en que i contenga un valor incorrecto o que el programa se comporte de forma errática. Las herramientas como AddressSanitizer (ASan) o UBSan pueden detectar desajustes de alineación, pero el error de aliasing suele ser un bug lógico silencioso. La forma segura de hacer esto en C es mediante una union o mediante memcpy.
N° 64