En informática , el juego de palabras es un término común para 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 ++ , construcciones como la conversión de tipo de puntero y (C ++ agrega conversión de tipo de referencia y a esta lista) se proporcionan para permitir muchos tipos de juegos de palabras, aunque algunos tipos no son compatibles con el lenguaje estándar.union
reinterpret_cast
En el lenguaje de programación Pascal , el uso de registros con variantes puede usarse para tratar un tipo de datos en particular de más de una manera, o de una manera normalmente no permitida.
Ejemplo de sockets
Un ejemplo clásico de juego de palabras 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 generalmente se llama de la siguiente manera:
struct sockaddr_in sa = { 0 }; int sockfd = ...; sa . sin_family = AF_INET ; sa . sin_port = htons ( puerto ); 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_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 se referirá al campo sa.sin_family
(donde sa
es de tipo struct sockaddr_in
). En otras palabras, la biblioteca de sockets usa juegos de palabras para implementar una forma rudimentaria de polimorfismo o herencia .
A menudo, en el mundo de la programación se ve el uso de estructuras de datos "acolchadas" para permitir el almacenamiento de diferentes tipos de valores en lo que es efectivamente el mismo espacio de almacenamiento. Esto se ve a menudo cuando dos estructuras se utilizan en exclusividad mutua para la optimización.
Ejemplo de punto flotante
No todos los ejemplos de juegos de palabras con tipos involucran estructuras, como lo hizo el ejemplo anterior. Suponga que queremos determinar si un número de coma flotante es negativo. Podríamos escribir:
bool is_negative ( float x ) { return x < 0.0 ; }
Sin embargo, suponiendo que las comparaciones de punto flotante sean caras, y también suponiendo que float
se represente de acuerdo con el estándar de punto flotante IEEE , y los enteros tengan 32 bits de ancho, podríamos utilizar juegos de palabras para extraer el bit de signo del número de punto flotante. usando solo operaciones enteras:
bool is_negative ( float x ) { unsigned int * ui = ( unsigned int * ) & x ; return * ui & 0x80000000 ; }
Tenga en cuenta que el comportamiento no será exactamente el mismo: en el caso especial de x
ser cero negativo , la primera implementación cede false
mientras que la segunda cede true
. Además, la primera implementación devolverá false
cualquier valor de NaN , pero la última podría devolver los true
valores de NaN con el bit de signo establecido.
Este tipo de juego de palabras es más peligroso que la mayoría. Mientras que el ejemplo anterior se basó solo en las garantías hechas por el lenguaje de programación C sobre el diseño de la estructura y la convertibilidad del puntero, el último ejemplo se basa en suposiciones sobre el hardware de un sistema en particular. Algunas situaciones, como el código de tiempo crítico que el compilador no puede optimizar , pueden requerir código peligroso. En estos casos, documentar todas estas suposiciones en los comentarios e introducir afirmaciones estáticas para verificar las expectativas de portabilidad ayuda a mantener el código en condiciones de mantenimiento .
Ejemplos prácticos de juegos de palabras en coma 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 incrementándolos 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 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 un expresión de tipo unsigned int
. En muchas plataformas comunes, este uso de juegos de palabras con punteros puede crear problemas si se alinean diferentes punteros de formas específicas de la máquina . Además, punteros de diferentes tamaños pueden alias accesos a la misma memoria , causando problemas que el compilador no verifica.
Uso de punteros
Se puede lograr un intento ingenuo de juegos de palabras con tipos mediante el uso de punteros:
flotador pi = 3,14159 ; uint32_t piAsRawData = * ( uint32_t * ) & pi ;
Según el estándar C, este código no debería (o mejor dicho, no tiene por qué) compilarse; sin embargo, si lo hace, piAsRawData
normalmente contiene los bits sin procesar de pi.
Uso de union
Es un error común intentar corregir los juegos de palabras mediante el uso de un union
. (El siguiente ejemplo asume además la representación de bits IEEE-754 para tipos de punto flotante).
bool is_negative ( float x ) { union { unsigned int ui ; flotar d ; } mi_unión = { . d = x }; return my_union . ui & 0x80000000 ; }
Acceder my_union.ui
después de inicializar el otro miembro, my_union.d
sigue siendo una forma de juego de palabras [4] en C y el resultado es un comportamiento no especificado [5] (y un comportamiento no definido en C ++ [6] ).
El lenguaje del § 6.5 / 7 [3] puede malinterpretarse para implicar que la lectura de miembros alternativos del sindicato está permitida. Sin embargo, el texto dice "Un objeto tendrá acceso a su valor almacenado sólo por ...". Es una expresión limitante, no una declaración de que se puede acceder a todos los posibles miembros del sindicato independientemente de cuál se almacenó por última vez. Por lo tanto, el uso de union
no evita ninguno de los problemas con el simple juego de palabras con un puntero directamente.
Incluso podría considerarse menos seguro que los juegos de palabras con tipos utilizando punteros, ya que será menos probable que un compilador informe una advertencia o error si no admite juegos de palabras con tipos.
Los compiladores como GCC admiten accesos de valor aliables como los ejemplos anteriores como una extensión de idioma. [7] En compiladores sin tal extensión, la regla de alias estricta se rompe solo por un memcpy explícito o usando un puntero char como un "intermediario" (ya que estos pueden ser alias libremente).
Para ver otro ejemplo de juego de palabras con tipos, consulte Stride of an array .
Pascal
Un registro de variante permite tratar un tipo de datos como varios tipos de datos según la variante a la que se hace referencia. En el siguiente ejemplo, se supone que el entero es de 16 bits, mientras que el entero y el real se supone que son 32, mientras que se supone que el carácter es de 8 bits:
escriba VariantRecord = caso de registro 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 ) ; terminar ; var V : VariantRecord ; K : entero ; LA : LongInt ; RA : Real ; Ch : Personaje ;V . I [ 1 ] : = 1 ; Ch : = V . C [ 1 ] ; (* Esto sería extraer el primer byte de VI *) V . R : = 8,3 ; LA : = V . L ; (* esto almacenaría un Real en un Entero *)
En Pascal, copiar un real a un entero lo convierte en el valor truncado. Este método traduciría el valor binario del número de punto flotante en lo que sea como un entero largo (32 bits), que no será el mismo y puede ser incompatible con el valor entero largo en algunos sistemas.
Estos ejemplos podrían usarse para crear conversiones extrañas, aunque, en algunos casos, puede haber usos legítimos para este tipo de construcciones, como para determinar la ubicación de piezas particulares de datos. En el siguiente ejemplo, se supone que un puntero y un signo largo son de 32 bits:
tipo PA = ^ Arec ; Arec = caso de registro RT : LongInt de 1 : ( P : PA ) ; 2 : ( L : LongInt ) ; terminar ; var PP : PA ; K : LongInt ;Nuevo ( PP ) ; PP ^. P : = PP ; WriteLn ( 'PP variable se encuentra en la dirección' , Hex ( PP ^. L )) ;
Donde "nuevo" es la rutina estándar en Pascal para asignar memoria para un puntero, y "hexadecimal" es presumiblemente una rutina para imprimir la cadena hexadecimal que describe el valor de un número 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 se asignan). 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 verificació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 en el que se ejecuta.
La técnica de reinterpretar cast de C / C ++ también funciona en Pascal. Esto puede ser útil cuando, por ejemplo, leyendo dwords de un flujo de bytes, y queremos tratarlos como flotantes. Aquí hay un ejemplo de trabajo, donde reinterpretamos y convertimos una dword a un flotante:
escriba pReal = ^ Real ;var DW : DWord ; F : Real ;F : = pReal ( @ DW ) ^;
C#
En C # (y otros lenguajes .NET), el tipo de juego de palabras es un poco más difícil de lograr debido al sistema de tipos, pero de todos modos se puede hacer usando punteros o uniones de estructuras.
Punteros
C # solo permite punteros a los llamados tipos nativos, es decir, cualquier tipo primitivo (excepto string
), enumeración, matriz o estructura que se componga solo de otros tipos nativos. Tenga en cuenta que los punteros solo se permiten en bloques de código marcados como "inseguros".
flotador pi = 3,14159 ; uint piAsRawData = * ( uint *) & pi ;
Uniones de estructura
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)] struct FloatAndUIntUnion { [FieldOffset (0)] public float DataAsFloat ; [FieldOffset (0)] public uint DataAsUInt ; }// ...FloatAndUIntUnion union ; unión . DataAsFloat = 3,14159 ; uint piAsRawData = unión . DataAsUInt ;
Código CIL sin procesar
Se puede usar CIL sin procesar en lugar de C #, porque 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 eludir con el siguiente código CIL:
. método public static hidebysig !! TEnum CombineEnums < valuetype . ctor ([ mscorlib ] System . ValueType ) TEnum > ( !! TEnum a , !! TEnum b ) cil managed { . maxstack 2 ldarg . 0 ldarg . 1 o // esto no provocará un desbordamiento, porque ayb 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 public static hidebysig uint8 [] ToByteArray < valuetype . ctor ([ mscorlib ] System . ValueType ) T > ( !! T & v // 'ref T' en C # ) cil administrado { . inicio de locales ( [0] uint8 [] ) . maxstack 3 // crea una nueva matriz de bytes con la longitud sizeof (T) y la almacena en local 0 sizeof !! T newarr uint8 dup // guarda una copia en la pila para después (1) stloc . 0 ldc . i4 . 0 ldelema uint8 // memcpy (local 0, & v, tamaño de (T)); // ldarg . 0 // esta es la * dirección * de 'v', porque su tipo es '!! T &' sizeof !! T cpblk ldloc . 0 ret }
Referencias
- ^ Herf, Michael (diciembre de 2001). "trucos radix" . estereopsis: gráficos .
- ^ "Trucos estúpidos de flotación" . ASCII aleatorio: blog tecnológico de Bruce Dawson . 24 de enero de 2012.
- ^ a b ISO / IEC 9899: 1999 s6.5 / 7
- ^ "§ 6.5.2.3/3, nota al pie 97", ISO / IEC 9899: 2018 (PDF) , 2018, p. 59, archivado desde el original (PDF) el 2018-12-30,
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 del La representación de 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 tipos" ). Esta podría ser una representación de trampa.
- ^ "§ J.1 / 1, viñeta 11", ISO / IEC 9899: 2018 (PDF) , 2018, p. 403, archivado desde el original (PDF) el 2018-12-30,
Los siguientes no están especificados:… Los valores de 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
- ^ GCC: No errores
enlaces externos
- Sección del manual de GCC sobre -fstrict-aliasing , que anula algunos juegos de palabras de tipo
- Defect Report 257 al estándar C99 , definiendo incidentalmente "juegos de palabras" en términos
union
y discutiendo los problemas que rodean el comportamiento definido por la implementación del último ejemplo anterior - Informe de defectos 283 sobre el uso de uniones para juegos de palabras tipográficas