En informática , el "type punning" es cualquier técnica de programación que subvierte o elude el sistema de tipos de un lenguaje de programación para lograr un efecto que sería difícil o imposible de lograr dentro de los límites del lenguaje formal .
En C y C++ , se proporcionan construcciones como la conversión de tipos de puntero y —C++ añade la conversión de tipos de referencia y a esta lista— para permitir muchos tipos de juegos de tipos, aunque algunos tipos no son realmente compatibles con el lenguaje estándar.unionreinterpret_cast
En el lenguaje de programación Pascal , el uso de registros con variantes puede utilizarse para tratar un tipo de dato particular de más de una manera, o de una manera que normalmente no está permitida.
Ejemplo de sockets
Un ejemplo clásico de juego de palabras con tipos se encuentra en la interfaz de sockets de Berkeley . La función para vincular un socket abierto pero no inicializado a una dirección IP se declara de la siguiente manera:
int bind ( int sockfd , struct sockaddr * my_addr , socklen_t addrlen );La bindfunción se suele llamar de la siguiente manera:
#include <sys/socket.h>struct sockaddr_in sa = { . sin_family = AF_INET , . sin_port = htons ( puerto ) };int sockfd = socket ( PF_INET , SOCK_STREAM , IPPROTO_TCP ); bind ( sockfd , ( struct sockaddr * ) & sa , sizeof sa );La biblioteca de sockets de Berkeley se basa fundamentalmente en el hecho de que en C , un puntero a struct sockaddr_ines libremente convertible a un puntero a struct sockaddr; y, además, que los dos tipos de estructura comparten la misma disposición de memoria. Por lo tanto, una referencia al campo de estructura my_addr->sin_family(donde my_addres de tipo struct sockaddr*) en realidad se referirá al campo sa.sin_family(donde saes de tipo struct sockaddr_in). En otras palabras, la biblioteca de sockets utiliza el juego de tipos para implementar una forma rudimentaria de polimorfismo o herencia .
En el mundo de la programación, es frecuente el uso de estructuras de datos "rellenadas" para almacenar diferentes tipos de valores en lo que, en la práctica, es el mismo espacio de almacenamiento. Esto suele ocurrir cuando se utilizan dos estructuras de forma mutuamente excluyente para optimizar el rendimiento.
Ejemplo de punto flotante
No todos los ejemplos de juegos de palabras con tipos involucran estructuras, como el ejemplo anterior. Supongamos que queremos determinar si un número de punto flotante es negativo. Podríamos escribir:
bool is_negative ( float x ) { return x < 0.0f ; }Sin embargo, suponiendo que las comparaciones de punto flotante son costosas, y también suponiendo que floatse representa de acuerdo con el estándar de punto flotante IEEE , y que los enteros tienen 32 bits de ancho, podríamos recurrir a la manipulación de tipos para extraer el bit de signo del número de punto flotante utilizando únicamente operaciones con enteros:
bool is_negative ( float x ) { int * i = ( int * ) & x ; return * i < 0 ; }Tenga en cuenta que el comportamiento no será exactamente el mismo: en el caso especial de xser cero negativo , la primera implementación produce falsemientras que la segunda produce true. Además, la primera implementación devolverá falsepara cualquier valor NaNtrue , pero la segunda podría devolver para valores NaN con el bit de signo activado. Por último, tenemos el problema de que el almacenamiento de los datos de punto flotante puede estar en orden de memoria big-endian o little-endian y, por lo tanto, el bit de signo podría estar en el byte menos significativo o en el más significativo. Por consiguiente, el uso de tipado con datos de punto flotante es un método cuestionable con resultados impredecibles.
Este tipo de manipulación de tipos es más peligrosa que la mayoría. Mientras que el ejemplo de sockets se basaba únicamente en las garantías del lenguaje de programación C sobre la estructura y la convertibilidad de punteros, este floatejemplo se basa en suposiciones sobre el hardware de un sistema en particular. La especificación del lenguaje C99 (ISO9899:1999) incluye la siguiente advertencia en la sección 6.3.2.3 Punteros : «Un puntero a un objeto o tipo incompleto puede convertirse en un puntero a un objeto o tipo incompleto diferente. Si el puntero resultante no está correctamente alineado con el tipo al que apunta, el comportamiento es indefinido». Por lo tanto, se debe tener mucho cuidado al usar la manipulación de tipos.
En ciertas situaciones, como en el caso de código crítico en cuanto al tiempo de ejecución que el compilador no optimiza , puede ser necesario ejecutar código peligroso. En estos casos, documentar todas estas suposiciones en comentarios e introducir aserciones estáticas para verificar las expectativas de portabilidad ayuda a mantener el código mantenible .
Ejemplos prácticos de juegos de palabras de punto flotante incluyen la raíz cuadrada inversa rápida popularizada por Quake III , la comparación rápida de FP como enteros, [ 1 ] y la búsqueda de valores vecinos mediante incremento como un entero (implementación nextafter). [ 2 ]
Por idioma
C y C++
Además de la suposición sobre la representación de bits de los números de punto flotante, el ejemplo anterior de juego de tipos de punto flotante también viola las restricciones del lenguaje C sobre cómo se accede a los objetos: [ 3 ] el tipo declarado de xes floatpero se lee a través de una expresión de tipo unsigned int. En muchas plataformas comunes, este uso del juego de punteros puede crear problemas si diferentes punteros están alineados de maneras específicas de la máquina . Además, los punteros de diferentes tamaños pueden alias de accesos a la misma memoria , causando problemas que no son verificados por el compilador. Sin embargo, incluso cuando el tamaño de los datos y la representación del puntero coinciden, los compiladores pueden confiar en las restricciones de no alias para realizar optimizaciones que serían inseguras en presencia de alias no permitidos.
Uso de punteros
Se puede lograr un intento ingenuo de juego de tipos utilizando punteros: (El siguiente ejemplo en ejecución asume la representación de bits IEEE-754 para el tipo float.)
// en C bool is_negative ( float x ) { int32_t i = * ( int32_t * ) & x ; return i < 0 ; }// en C++ bool is_negative ( float x ) { int32_t i = * reinterpret_cast < int32_t *> ( & x ); return i < 0 ; }Las reglas de alias del estándar C establecen que el valor almacenado de un objeto solo debe ser accedido por una expresión lvalue de un tipo compatible. [ 4 ] Los tipos floaty int32_tno son compatibles, por lo tanto, el comportamiento de este código es indefinido . Aunque en GCC y LLVM este programa en particular compila y se ejecuta como se espera, ejemplos más complejos pueden interactuar con las suposiciones hechas por el alias estricto y conducir a un comportamiento no deseado. La opción -fno-strict-aliasinggarantizará el comportamiento correcto del código que utiliza esta forma de juego de tipos, aunque se recomienda utilizar otras formas de juego de tipos. [ 5 ]
Uso deunion
En C, pero no en C++, a veces es posible realizar el juego de tipos a través de un union.
bool is_negative ( float x ) { union { int i ; float f ; } my_union ; my_union.f = x ; return my_union.i < 0 ; }Acceder my_union.ia después de la escritura más reciente en el otro miembro, my_union.f, es una forma permitida de juego de tipos en C, [ 6 ] siempre que el miembro leído no sea mayor que aquel cuyo valor se estableció (de lo contrario, la lectura tiene un comportamiento no especificado [ 7 ] ). Lo mismo es sintácticamente válido pero tiene un comportamiento indefinido en C++, [ 8 ] donde solo el último miembro escrito de un unionse considera que tiene algún valor.
Para otro ejemplo de juego de palabras tipográfico, consulte Paso de una matriz .
Uso dememcpy
memcpyes un método seguro y portátil de juego de tipos, [ 9 ] bendecido en el estándar C++. [ 10 ] Clang y GCC incluyen optimizaciones específicas para memcpyllamadas de este tipo. [ 11 ]
#include <string.h>bool is_negative ( float x ) { int i ; memcpy ( & i , & x , sizeof ( int )); // o std::memcpy en C++ return i < 0 ; }Uso debit_cast
En C++20 , la std::bit_castfunción permite el juego de tipos sin comportamiento indefinido. También permite que la función sea etiquetada constexpr. [ 10 ] La implementación de referencia es un envoltorio alrededor de std::memcpy. [ 12 ]
importar std ;usando std :: numeric_limits ;constexpr bool is_negative ( float x ) noexcept { static_assert ( numeric_limits < float >:: is_iec559 ); // (habilitar solo en IEEE 754) int32_t i = std :: bit_cast < int32_t > ( x ); return i < 0 ; }Otro tipo de mecanismo de juego de palabras incluido en C++ en C++23 con y . [ 13 ]std::start_lifetime_asstd::start_lifetime_as_array
Pascal
Un registro de variante permite tratar un tipo de dato como varios tipos de datos dependiendo de la variante a la que se haga referencia. En el siguiente ejemplo, se presume que un entero es de 16 bits, mientras que un entero largo y un entero real son de 32 bits, y un carácter es de 8 bits:
type VariantRecord = record case RecType : LongInt of 1 : ( I : array [ 1 .. 2 ] of Integer ) ; (* no se muestra aquí: puede haber varias variables en la instrucción case de un registro variante *) 2 : ( L : LongInt ) ; 3 : ( R : Real ) ; 4 : ( C : array [ 1 .. 4 ] of Char ) ; end ;var V : VariantRecord ; K : Integer ; LA : LongInt ; RA : Real ; Ch : Character ;V . I [ 1 ] := 1 ; Ch := V . C [ 1 ] ; (* esto extraería el primer byte de VI *) V . R := 8.3 ; LA := V . L ; (* esto almacenaría un Real en un Integer *)En Pascal, copiar un número real a un entero lo convierte a su valor truncado. Este método traduciría el valor binario del número de punto flotante a su equivalente como un entero largo (32 bits), que no será el mismo y podría ser incompatible con el valor entero largo en algunos sistemas.
Estos ejemplos podrían utilizarse para crear conversiones extrañas, aunque, en algunos casos, este tipo de construcciones pueden tener usos legítimos, como determinar la ubicación de datos específicos. En el siguiente ejemplo, se supone que tanto un puntero como un longint son de 32 bits:
tipo PA = ^ Arec ;Arec = registro caso RT : LongInt de 1 : ( P : PA ) ; 2 : ( L : LongInt ) ; fin ;var PP : PA ; K : LongInt ;Nuevo ( PP ) ; PP ^. P := PP ; WriteLn ( 'La variable PP se encuentra en la dirección ' , Hex ( PP ^. L )) ;Donde "new" es la rutina estándar en Pascal para asignar memoria a un puntero, y "hex" es presumiblemente una rutina para imprimir la cadena hexadecimal que describe el valor de un entero. Esto permitiría mostrar la dirección de un puntero, algo que normalmente no está permitido. (Los punteros no se pueden leer ni escribir, solo asignar). Asignar un valor a una variante entera de un puntero permitiría examinar o escribir en cualquier ubicación de la memoria del sistema:
PP ^. L := 0 ; PP := PP ^. P ; (* PP ahora apunta a la dirección 0 *) K := PP ^. L ; (* K contiene el valor de la palabra 0 *) WriteLn ( 'La palabra 0 de esta máquina contiene ' , K ) ;Esta construcción puede provocar una comprobación del programa o una violación de la protección si la dirección 0 está protegida contra la lectura en la máquina en la que se ejecuta el programa o en el sistema operativo bajo el que se ejecuta.
La técnica de conversión reinterpretada de C/C++ también funciona en Pascal. Esto puede ser útil, por ejemplo, al leer dwords de un flujo de bytes y querer tratarlos como float. Aquí hay un ejemplo práctico donde convertimos un dword a float mediante la técnica de conversión reinterpretada:
tipo pReal = ^ Real ;var DW : DWord ; F : Real ;F := pReal ( @ DW ) ^;DO#
En C# (y otros lenguajes .NET), la manipulación de tipos es un poco más difícil de lograr debido al sistema de tipos, pero se puede hacer de todos modos, utilizando punteros o uniones de estructuras.
Consejos
C# solo permite punteros a los llamados tipos nativos, es decir, cualquier tipo primitivo (excepto string), enumeración, matriz o estructura compuesta únicamente por otros tipos nativos. Tenga en cuenta que los punteros solo están permitidos en bloques de código marcados como 'unsafe'.
inseguro { float pi = 3.14159 ; uint piAsRawData = * ( uint * ) & pi ; }sindicatos estructurales
Se permiten las uniones de estructuras sin que exista ninguna noción de código "inseguro", pero sí requieren la definición de un nuevo tipo.
[StructLayout(LayoutKind.Explicit)] struct FloatAndUIntUnion { [FieldOffset(0)] public float DataAsFloat ;[FieldOffset(0)] public uint DataAsUInt ; }// ...Unión FloatAndUIntUnion ; unión.DataAsFloat = 3.14159 ; uint piAsRawData = unión.DataAsUInt ;Código CIL sin procesar
Se puede usar CIL puro en lugar de C#, ya que no tiene la mayoría de las limitaciones de tipos. Esto permite, por ejemplo, combinar dos valores de enumeración de un tipo genérico:
TEnum a = ...; TEnum b = ...; TEnum combinado = a | b ; // ilegalEsto se puede eludir mediante el siguiente código CIL:
. método público estático hidebysig !! TEnum CombineEnums < valuetype . ctor ([ mscorlib ] System . ValueType ) TEnum > ( !! TEnum a , !! TEnum b ) cil managed { . maxstack 2ldarg .0 ldarg .1 o // esto no causará un desbordamiento, porque a y b tienen el mismo tipo y, por lo tanto, el mismo tamaño. ret }El código de operacióncpblk CIL permite algunos otros trucos, como convertir una estructura en una matriz de bytes:
. método público estático hidebysig uint8 [] ToByteArray < valuetype . ctor ([ mscorlib ] System . ValueType ) T > ( !! T & v // 'ref T' en C# ) cil managed { . locals init ( [0] uint8 [] ). maxstack 3// crea una nueva matriz de bytes con longitud sizeof(T) y la almacena en local 0 sizeof !! T newarr uint8 dup // guarda una copia en la pila para más tarde (1) stloc .0ldc . i4 .0 ldelema uint8// memcpy(local 0, &v, sizeof(T)); // <el array todavía está en la pila, ver (1)> ldarg .0 // esta es la *dirección* de 'v', porque su tipo es '!!T&' sizeof !! T cpblkldloc .0 ret }Java
A diferencia de C/C++, que ofrecen acceso a memoria y aritmética de punteros, Java no los admite (oficialmente). Sin embargo, la manipulación de tipos se puede simular de forma similar.
Usandojava.nio.ByteBuffer
import java.nio.ByteBuffer ;void main ( String [] args ) { int value = 42 ;ByteBuffer buffer = ByteBuffer.allocate ( Integer.BYTES ) ; buffer.putInt ( value ) ;// "Convierte" estos datos en un float (solo como ejemplo) buffer . flip (); float punResult = buffer . getFloat (); }Usandosun.misc.Unsafe
Utilizando sun.misc.Unsafe , se puede realizar el juego de tipos directamente, utilizando el acceso directo a la memoria .
import java.lang.reflect.Field ; import sun.misc.Unsafe ;void main ( String [] args ) throws NoSuchFieldException , IllegalAccessException { Field f = Unsafe . class . getDeclaredField ( "theUnsafe" ); f . setAccessible ( true ); Unsafe unsafe = ( Unsafe ) f . get ( null );dirección larga = unsafe.allocateMemory ( 4 ) ; unsafe.putInt ( dirección , 42 ) ;// Interpreta la ubicación de memoria como un float float result = unsafe . getFloat ( address ); unsafe . freeMemory ( address ); }Referencias
- ↑ Herf, Michael (diciembre de 2001). "Trucos de radix" . stereopsis : graphics .
- ↑ "Trucos tontos con flotadores" . Blog tecnológico de Bruce Dawson, en formato ASCII aleatorio . 24 de enero de 2012.
- ↑ ISO/IEC 9899:1999 s6.5/7
- ↑ "§ 6.5/7" (PDF) , ISO/IEC 9899:2018 , 2018, pág. 55, archivado del original (PDF) el 30-12-2018,
El valor almacenado de un objeto solo podrá ser accedido mediante una expresión lvalue que tenga uno de los siguientes tipos: [...]
- ↑ "Errores de GCC - Proyecto GNU" . gcc.gnu.org .
- ↑ "§ 6.5.2.3/3, nota al pie 97" (PDF) , ISO/IEC 9899:2018 , 2018, pág. 59, archivado del original (PDF) el 30-12-2018,
Si el miembro utilizado para leer el contenido de un objeto de unión no es el mismo que el miembro utilizado por última vez para almacenar un valor en el objeto, la parte apropiada de la representación del objeto del valor se reinterpreta como una representación de objeto en el nuevo tipo como se describe en 6.2.6 (
un proceso a veces llamado "juego de palabras de tipo"
).
Esto podría ser una representación trampa.
- ↑ "§ J.1/1, punto 11" (PDF) , ISO/IEC 9899:2018 , 2018, pág. 403, archivado del original (PDF) el 30-12-2018,
Los siguientes no están especificados: … Los valores de los bytes que corresponden a miembros de la unión
distintos del último almacenado en
(6.2.6.1).
- ↑ ISO/IEC 14882:2011 Sección 9.5
- ↑ "Alias en C y C++" . developer.arm.com . Ejemplo: Usar memcpy() para la conversión segura de tipos . Consultado el 15 de junio de 2025 .
- 1 2 "Representaciones de objetos mediante conversión de bits" . www.open-std.org . Archivado del original el 18 de agosto de 2018. Consultado el 10 de noviembre de 2018 .
- ↑ Regehr, John (11 de junio de 2013). "Juego de palabras de tipos, alias estrictos y optimización" . Embedded in Academia . Recuperado el 22 de marzo de 2026.
Ambos compiladores entienden memcpy lo suficientemente bien como para que obtengamos el código objeto deseado [...] Se convirtió en mi modismo preferido para el juego de palabras de tipos hace unos años cuando descubrí que los compiladores podían ver a través de memcpy y generar el código correcto.
- ↑ "bit_cast/bit_cast.h en master · jfbastien/bit_cast" . GitHub . Consultado el 15 de junio de 2025 .
- ↑ "std::start_lifetime_as, std::start_lifetime_as_array" . cppreference.com . cppreference.com . Consultado el 9 de mayo de 2026 .
Enlaces externos
- Sección del manual de GCC sobre
-fstrict-aliasing, que desbarata algún tipo de juego de palabras - Informe de defectos 257 al estándar C99 , que define incidentalmente "type punning" en términos de
union, y analiza los problemas relacionados con el comportamiento definido por la implementación del último ejemplo anterior. - Informe de defectos 283 sobre el uso de uniones para la manipulación de tipos
- Estructuras de programación