Este artículo tiene varios problemas. Ayúdenos a mejorarlo o a discutir estos problemas en la página de discusión . ( Aprenda cómo y cuándo eliminar estos mensajes )
|
En informática , un juego de palabras tipográfico 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 tipo puntero y —C++ agrega la conversión de tipo de referencia y a esta lista— para permitir muchos tipos de juegos de palabras con tipos, aunque algunos tipos en realidad no son compatibles con el lenguaje estándar.union
reinterpret_cast
En el lenguaje de programación Pascal , el uso de registros con variantes se puede utilizar para tratar un tipo de datos particular de más de una manera, o de una manera normalmente no permitida.
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 bind
función normalmente se llama de la siguiente manera:
estructura sockaddr_in sa = { 0 }; int calcetín = ...; sa . sin_familia = AF_INET ; sa . sin_port = htons ( puerto ); enlazar ( 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_in
se puede convertir libremente en un puntero a struct sockaddr
; y, además, que los dos tipos de estructura comparten el mismo diseño de memoria. Por lo tanto, una referencia al campo de estructura my_addr->sin_family
(donde my_addr
es de tipo struct sockaddr*
) en realidad hará referencia al campo sa.sin_family
(donde sa
es de tipo struct sockaddr_in
). En otras palabras, la biblioteca de sockets utiliza un juego de palabras con 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 permitir el almacenamiento de distintos tipos de valores en lo que es, en realidad, el mismo espacio de almacenamiento. Esto se observa a menudo cuando se utilizan dos estructuras en exclusividad mutua para la optimización.
No todos los ejemplos de juegos de palabras con tipos implican estructuras, como en el ejemplo anterior. Supongamos que queremos determinar si un número de punto flotante es negativo. Podríamos escribir:
bool is_negative ( float x ) { devolver x < 0.0f ; }
Sin embargo, suponiendo que las comparaciones de punto flotante son costosas, y también suponiendo que float
se representa de acuerdo con el estándar de punto flotante IEEE , y los números enteros tienen 32 bits de ancho, podríamos realizar juegos de palabras con tipos para extraer el bit de signo del número de punto flotante utilizando solo operaciones con números enteros:
bool es_negativo ( 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 x
ser cero negativo , la primera implementación produce , false
mientras que la segunda produce true
. Además, la primera implementación retornará false
para cualquier valor NaNtrue
, pero la última podría retornarse para valores NaN con el bit de signo establecido.
Este tipo de juego de palabras es más peligroso que la mayoría. Mientras que el primer ejemplo se basó únicamente en garantías realizadas por el lenguaje de programación C sobre el diseño de la estructura y la convertibilidad de los punteros, el segundo ejemplo se basa en suposiciones sobre el hardware de un sistema en particular. Algunas situaciones, como el código crítico en el tiempo que el compilador no puede optimizar de otro modo , pueden requerir código peligroso. En estos casos, documentar todas esas suposiciones en comentarios e introducir afirmaciones estáticas para verificar las expectativas de portabilidad ayuda a mantener el código mantenible .
Algunos ejemplos prácticos de juegos de palabras con punto flotante incluyen la raíz cuadrada inversa rápida popularizada por Quake III , la comparación rápida de FP como números enteros, [1] y la búsqueda de valores vecinos incrementando como un número entero (implementando nextafter
). [2]
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 palabras de tipo de punto flotante también viola las restricciones del lenguaje C sobre cómo se accede a los objetos: [3] el tipo declarado de x
es float
pero se lee a través de una expresión de tipo unsigned int
. En muchas plataformas comunes, este uso de juego de palabras de puntero puede crear problemas si diferentes punteros se alinean de formas específicas de la máquina . Además, los punteros de diferentes tamaños pueden crear alias de accesos a la misma memoria , lo que causa problemas que el compilador no controla. 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 permitido.
Se puede lograr un intento ingenuo de hacer un juego de palabras con tipos mediante el uso de punteros: (El siguiente ejemplo en ejecución asume una representación de bits IEEE-754 para el tipo float
).
bool is_negative ( float x ) { int32_t i = * ( int32_t * ) & x ; // En C++ esto es equivalente a: int32_t i = *reinterpret_cast<int32_t*>(&x); return i < 0 ; }
Las reglas de aliasing del estándar C establecen que el valor almacenado de un objeto solo se podrá acceder mediante una expresión lvalue de un tipo compatible. [4] Los tipos float
y int32_t
no son compatibles, por lo tanto, el comportamiento de este código es undefined . Aunque en GCC y LLVM este programa en particular se compila y se ejecuta como se espera, ejemplos más complicados pueden interactuar con suposiciones hechas por aliasing estricto y conducir a un comportamiento no deseado. La opción -fno-strict-aliasing
garantizará el comportamiento correcto del código que utiliza esta forma de juego de palabras de tipo, aunque se recomienda utilizar otras formas de juego de palabras de tipo. [5]
union
En C, pero no en C++, a veces es posible realizar juegos de palabras de tipos mediante un union
.
bool es_negativo ( float x ) { unión { int i ; float d ; } mi_unión ; mi_unión . d = x ; devolver mi_unión . i < 0 ; }
El acceso my_union.i
después de la última escritura en el otro miembro, my_union.d
, es una forma permitida de juego de palabras 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] sin embargo, donde solo se considera que el último miembro escrito de a union
tiene algún valor.
Para ver otro ejemplo de juego de palabras de tipos, consulte Stride de una matriz .
bit_cast
En C++20 , la std::bit_cast
función permite el uso de juegos de palabras con tipos sin comportamiento indefinido. También permite que la función tenga etiquetas constexpr
.
constexpr bool is_negative ( float x ) noexcept { static_assert ( std :: numeric_limits < float >:: is_iec559 ); // (habilitar solo en IEEE 754) auto i = std :: bit_cast < std :: int32_t > ( x ); return i < 0 ; }
Un registro de variante permite tratar un tipo de datos como varios tipos de datos según la variante a la que se haga referencia. En el siguiente ejemplo, se supone que un entero tiene 16 bits, mientras que un entero largo y un real tienen 32 bits y un carácter tiene 8 bits:
tipo VariantRecord = registro caso RecType : LongInt de 1 : ( I : matriz [ 1 .. 2 ] de Integer ) ; (* no se muestra aquí: puede haber varias variables en la declaración de caso de un registro de variante *) 2 : ( L : LongInt ) ; 3 : ( R : Real ) ; 4 : ( C : matriz [ 1 .. 4 ] de Char ) ; fin ; var V : VariantRecord ; K : Entero ; LA : LongInt ; RA : Real ; Ch : Carácter ; 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 entero *)
En Pascal, copiar un número real a un entero lo convierte en el valor truncado. Este método traduciría el valor binario del número de punto flotante a lo que sea como un entero largo (32 bits), que no será lo mismo y puede ser incompatible con el valor entero largo en algunos sistemas.
Estos ejemplos se pueden utilizar para crear conversiones extrañas, aunque, en algunos casos, puede haber usos legítimos para este tipo de construcciones, como por ejemplo para determinar la ubicación de fragmentos de datos concretos. En el siguiente ejemplo, se supone que tanto un puntero como un longint tienen 32 bits:
tipo PA = ^ Arec ; Arec = caso de registro RT : LongInt de 1 : ( P : PA ) ; 2 : ( L : LongInt ) ; fin ; var PP : PA ; K : Entero largo ; 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 la visualización de 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 en 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 violación de verificación o protección del programa si la dirección 0 está protegida contra 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 reinterpretación de conversión de C/C++ también funciona en Pascal. Esto puede ser útil, por ejemplo, cuando leemos dwords de un flujo de bytes y queremos tratarlos como flotantes. Aquí hay un ejemplo práctico, donde reinterpretamos y convertimos una dword en un flotante:
tipo pReal = ^ Real ; var DW : DWord ; F : Real ; F := pReal ( @ DW ) ^;
En C# (y otros lenguajes .NET), el juego de palabras entre tipos es un poco más difícil de lograr debido al sistema de tipos, pero se puede hacer de todos modos, usando punteros o uniones de estructuras.
C# solo permite punteros a los denominados tipos nativos, es decir, cualquier tipo primitivo (excepto string
), enumeración, matriz o estructura que esté compuesto únicamente por otros tipos nativos. Tenga en cuenta que los punteros solo se permiten en bloques de código marcados como "inseguros".
flotante pi = 3.14159 ; uint piAsRawData = * ( uint * ) & pi ;
Las uniones de estructuras están permitidas sin ninguna noción de código "inseguro", pero requieren la definición de un nuevo tipo.
[StructLayout(LayoutKind.Explicit)] estructura FloatAndUIntUnion { [FieldOffset(0)] público flotante DataAsFloat ; [FieldOffset(0)] uint público DataAsUInt ; } // ...FloatAndUIntUnion unión ; unión.DataAsFloat = 3.14159 ; uint piAsRawData = unión.DataAsUInt ;
Se puede utilizar CIL sin formato en lugar de C#, ya que no tiene la mayoría de las limitaciones de tipo. Esto permite, por ejemplo, combinar dos valores de enumeración de un tipo genérico:
TEnum a = ...; TEnum b = ...; TEnum combinado = a | b ; // ilegal
Esto se puede evitar con el siguiente código CIL:
. método public static hidebysig !! TEnum CombineEnums < tipo_valor . ctor ([ mscorlib ] System . ValueType ) TEnum > ( !! TEnum a , !! TEnum b ) cil administrado { . maxstack 2 ldarg . 0 ldarg . 1 o // esto no provocará un desbordamiento, porque a y b tienen el mismo tipo y, por lo tanto, el mismo tamaño. ret }
El cpblk
código de operación CIL permite algunos otros trucos, como convertir una estructura en una matriz de bytes:
. método público estático hidebysig uint8 [] ToByteArray < tipo_valor . ctor ([ mscorlib ] System . ValueType ) T > ( !! T & v // 'ref T' en C# ) cil administrado { . locals init ( [0] uint8 [] ) .máximo stack 3 // crea una nueva matriz de bytes con longitud sizeof(T) y almacénala en local 0 sizeof !! T newarr uint8 dup // mantén una copia en la pila para más tarde (1) stloc . 0 ldc . i4 . 0 ldelema uint8 // memcpy(local 0, &v, sizeof(T)); // <la matriz todavía está en la pila, ver (1)> ldarg . 0 // esta es la *dirección* de 'v', porque su tipo es '!!T&' sizeof !! T cpblk ldloc . 0 ret }
El valor almacenado de un objeto solo podrá ser accedido por una expresión lvalue que tenga uno de los siguientes tipos: [...]
Si el miembro utilizado para leer el contenido de un objeto de unión no es el mismo que el último miembro utilizado para almacenar un valor en el objeto, la parte apropiada de la representación del objeto del valor se reinterpreta como una representación del 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.
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).
-fstrict-aliasing
, que derrota algunos juegos de palabras de tipounion
y analiza los problemas relacionados con el comportamiento definido por la implementación del último ejemplo anterior.