В информатике каламбуром называется любой метод программирования, который подрывает или обходит систему типов языка программирования с целью достижения эффекта, которого было бы трудно или невозможно достичь в рамках формального языка.
В C и C++ такие конструкции, как преобразование типа указателя и (в C++ к этому списку добавлено преобразование типа ссылки и ), предусмотрены для того, чтобы разрешить множество видов каламбуров с типами, хотя некоторые виды на самом деле не поддерживаются стандартным языком.union
reinterpret_cast
В языке программирования Pascal использование записей с вариантами может применяться для обработки определенного типа данных более чем одним способом или способом, который обычно не допускается.
Классический пример каламбуров типа можно найти в интерфейсе сокетов Беркли . Функция для привязки открытого, но неинициализированного сокета к IP-адресу объявляется следующим образом:
int bind ( int sockfd , struct sockaddr * my_addr , socklen_t addrlen );
Функция bind
обычно вызывается следующим образом:
структура sockaddr_in sa = {0} ; ИНТ Sockfd = ...; са . sin_family = AF_INET ; са . sin_port = htons ( порт ); привязать ( sockfd , ( struct sockaddr * ) & sa , sizeof sa );
Библиотека сокетов Беркли в своей основе опирается на тот факт, что в C указатель на struct sockaddr_in
свободно преобразуется в указатель на struct sockaddr
; и, кроме того, что два типа структур разделяют одну и ту же схему памяти. Поэтому ссылка на поле структуры my_addr->sin_family
(где my_addr
имеет тип struct sockaddr*
) фактически будет ссылаться на поле sa.sin_family
(где sa
имеет тип struct sockaddr_in
). Другими словами, библиотека сокетов использует каламбур типа для реализации элементарной формы полиморфизма или наследования .
В мире программирования часто можно увидеть использование "дополненных" структур данных, позволяющих хранить различные типы значений в том, что фактически является одним и тем же пространством хранения. Это часто можно увидеть, когда две структуры используются во взаимоисключающем режиме для оптимизации.
Не все примеры каламбуров типа включают структуры, как в предыдущем примере. Предположим, мы хотим определить, является ли число с плавающей точкой отрицательным. Мы могли бы написать:
bool is_negative ( float x ) { return x < 0.0f ; }
Однако, предположив, что сравнения с плавающей точкой требуют больших затрат, а также предположив, что float
представлено в соответствии со стандартом IEEE для чисел с плавающей точкой , а целые числа имеют ширину 32 бита, мы могли бы заняться каламбуром типов, чтобы извлечь знаковый бит числа с плавающей точкой, используя только целочисленные операции:
bool is_negative ( float x ) { int * i = ( int * ) & x ; return * i < 0 ; }
Обратите внимание, что поведение не будет точно таким же: в особом случае x
отрицательного нуля первая реализация выдает , false
а вторая выдает true
. Кроме того, первая реализация вернет false
для любого значения NaN , но последняя может вернуть true
для значений NaN с установленным битом знака.
Этот вид каламбуров более опасен, чем большинство других. В то время как первый пример опирался только на гарантии языка программирования C относительно структуры и конвертируемости указателей, последний пример опирается на предположения об аппаратном обеспечении конкретной системы. Некоторые ситуации, такие как критический по времени код, который компилятор в противном случае не сможет оптимизировать , могут потребовать опасного кода. В этих случаях документирование всех таких предположений в комментариях и введение статических утверждений для проверки ожиданий переносимости помогает поддерживать код в поддерживаемом состоянии .
Практические примеры каламбуров с плавающей точкой включают быстрое извлечение обратного квадратного корня , популяризированное Quake III , быстрое сравнение чисел с плавающей точкой как целых чисел [1] и поиск соседних значений путем приращения как целого числа (реализация nextafter
). [2]
В дополнение к предположению о битовом представлении чисел с плавающей точкой, приведенный выше пример каламбуров с плавающей точкой также нарушает ограничения языка C на то, как осуществляется доступ к объектам: [3] объявленный тип x
равен float
, но он считывается через выражение типа unsigned int
. На многих распространенных платформах такое использование каламбуров указателей может создавать проблемы, если разные указатели выровнены машинно-специфичными способами . Кроме того, указатели разных размеров могут создавать псевдонимы доступа к одной и той же памяти , вызывая проблемы, которые не проверяются компилятором. Однако даже когда размер данных и представление указателя совпадают, компиляторы могут полагаться на ограничения несовпадения для выполнения оптимизаций, которые были бы небезопасны при наличии запрещенного совмещения.
Наивную попытку каламбурного типа можно осуществить с помощью указателей: (В следующем примере предполагается битовое представление IEEE-754 для типа float
.)
bool is_negative ( float x ) { int32_t i = * ( int32_t * ) & x ; // В C++ это эквивалентно: int32_t i = *reinterpret_cast<int32_t*>(&x); return i < 0 ; }
Правила псевдонимов стандарта C гласят, что объект должен иметь доступ к своему сохраненному значению только с помощью выражения lvalue совместимого типа. [4] Типы float
и int32_t
несовместимы, поэтому поведение этого кода не определено . Хотя в GCC и LLVM эта конкретная программа компилируется и работает как и ожидалось, более сложные примеры могут взаимодействовать с предположениями, сделанными строгим псевдонимом , и приводить к нежелательному поведению. Эта опция -fno-strict-aliasing
обеспечит правильное поведение кода, использующего эту форму каламбура типа, хотя рекомендуется использовать другие формы каламбура типа. [5]
union
В языке C, но не в C++, иногда можно выполнить каламбур типа с помощью union
.
bool is_negative ( float x ) { union { int i ; float d ; } my_union ; my_union . d = x ; return my_union . i < 0 ; }
Доступ my_union.i
после последней записи в другой член, my_union.d
, является допустимой формой каламбуров в C, [6] при условии, что прочитанный член не больше того, значение которого было установлено (в противном случае чтение имеет неопределенное поведение [7] ). То же самое синтаксически допустимо, но имеет неопределенное поведение в C++, [8] однако, где только последний записанный член a union
считается имеющим какое-либо значение вообще.
Другой пример каламбуров см. в разделе «Шаг массива» .
bit_cast
В C++20 функция std::bit_cast
позволяет каламбурить с типом без неопределенного поведения. Она также позволяет маркировать функцию constexpr
.
constexpr bool is_negative ( float x ) noexcept { static_assert ( std :: numeric_limits < float >:: is_iec559 ); // (включено только в IEEE 754) auto i = std :: bit_cast < std :: int32_t > ( x ); return i < 0 ; }
Запись варианта позволяет обрабатывать тип данных как несколько видов данных в зависимости от того, на какой вариант ссылаются. В следующем примере предполагается , что целое число имеет размер 16 бит, тогда как longint и real — 32, а character — 8 бит:
type VariantRecord = record case RecType : LongInt of 1 : ( I : array [ 1 .. 2 ] of Integer ) ; (* здесь не показано: в операторе case вариантной записи может быть несколько переменных *) 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 ] ; (* это извлечет первый байт VI *) V . R := 8.3 ; LA := V . L ; (* это сохранит Real в Integer *)
В Pascal копирование вещественного числа в целое преобразует его в усеченное значение. Этот метод переведет двоичное значение числа с плавающей точкой в то, что оно есть, как длинное целое число (32 бита), что не будет тем же самым и может быть несовместимо со значением длинного целого числа в некоторых системах.
Эти примеры можно использовать для создания странных преобразований, хотя в некоторых случаях могут быть законные применения для этих типов конструкций, например, для определения местоположений определенных фрагментов данных. В следующем примере предполагается, что указатель и longint являются 32-битными:
тип PA = ^ Arec ; Arec = запись случая RT : LongInt из 1 : ( P : PA ) ; 2 : ( L : LongInt ) ; конец ; var PP : PA ; K : LongInt ; New ( PP ) ; PP ^ .P := PP ; WriteLn ( 'Переменная PP расположена по адресу' , Hex ( PP ^ .L )) ;
Где "new" — это стандартная процедура в Pascal для выделения памяти для указателя, а "hex" — это, предположительно, процедура для печати шестнадцатеричной строки, описывающей значение целого числа. Это позволило бы отобразить адрес указателя, что обычно не допускается. (Указатели нельзя читать или записывать, их можно только назначать.) Назначение значения целочисленному варианту указателя позволило бы исследовать или записывать в любое место в системной памяти:
PP ^. L := 0 ; PP := PP ^. P ; (* PP теперь указывает на адрес 0 *) K := PP ^. L ; (* K содержит значение слова 0 *) WriteLn ( 'Слово 0 этой машины содержит ' , K ) ;
Эта конструкция может вызвать проверку программы или нарушение защиты, если адрес 0 защищен от чтения на машине, на которой запущена программа, или в операционной системе, в которой она запущена.
Техника переинтерпретации приведения из C/C++ также работает в Pascal. Это может быть полезно, например, когда считываются dword из потока байтов, и мы хотим рассматривать их как float. Вот рабочий пример, где мы переинтерпретируем-преобразуем dword в float:
тип pReal = ^ Real ; var DW : DWord ; F : Вещественное ; F := pReal ( @ DW ) ^;
В C# (и других языках .NET) каламбур типов реализовать немного сложнее из-за системы типов, но тем не менее его можно реализовать с помощью указателей или структурных объединений.
C# допускает указатели только на так называемые собственные типы, т. е. любой примитивный тип (кроме string
), enum, array или struct, который состоит только из других собственных типов. Обратите внимание, что указатели разрешены только в блоках кода, помеченных как «небезопасные».
float pi = 3.14159 ; uint piAsRawData = * ( uint * ) & pi ;
Объединения структур разрешены без какого-либо понятия «небезопасного» кода, но они требуют определения нового типа.
[StructLayout(LayoutKind.Explicit)] struct FloatAndUIntUnion { [FieldOffset(0)] public float DataAsFloat ; [FieldOffset(0)] public uint DataAsUInt ; } // ...FloatAndUIntUnion union ; union.DataAsFloat = 3.14159 ; uint piAsRawData = union.DataAsUInt ;
Вместо C# можно использовать сырой CIL , поскольку он не имеет большинства ограничений типов. Это позволяет, например, объединить два значения enum универсального типа:
TEnum a = ...; TEnum b = ...; TEnum combined = a | b ; // недопустимо
Это можно обойти с помощью следующего CIL-кода:
. метод public static hidebysig !! TEnum CombineEnums < valuetype . ctor ([ mscorlib ] System . ValueType ) TEnum > ( !! TEnum a , !! TEnum b ) cil управляемый { . maxstack 2 ldarg . 0 ldarg . 1 или // это не вызовет переполнения, поскольку a и b имеют одинаковый тип и, следовательно, одинаковый размер. ret }
Код cpblk
операции CIL допускает и другие трюки, например, преобразование структуры в массив байтов:
. method public static hidebysig uint8 [] ToByteArray < valuetype . ctor ([ mscorlib ] System . ValueType ) T > ( !! T & v // 'ref T' в C# ) cil managed { . locals init ( [0] uint8 [] ) . максстэк 3 // создаем новый массив байтов длиной sizeof(T) и сохраняем его локально 0 sizeof !! T newarr uint8 dup // сохраняем копию в стеке для дальнейшего использования (1) stloc . 0 ldc . i4 . 0 ldelema uint8 // memcpy(local 0, &v, sizeof(T)); // <массив все еще находится в стеке, см. (1)> ldarg . 0 // это *адрес* 'v', потому что его тип '!!T&' sizeof !! T cpblk ldloc . 0 ret }
Доступ к сохраненному значению объекта может осуществляться только с помощью выражения lvalue, имеющего один из следующих типов: [...]
Если член, используемый для чтения содержимого объекта union, не совпадает с членом, который в последний раз использовался для хранения значения в объекте, соответствующая часть объектного представления значения переинтерпретируется как объектное представление в новом типе, как описано в 6.2.6 (
процесс, иногда называемый "каламбуром типа"
).
Это может быть представлением ловушки.
Следующие данные не указаны: … Значения байтов, которые соответствуют членам объединения,
отличным от последнего, сохраненного в
(6.2.6.1).
-fstrict-aliasing
, который устраняет некоторые каламбурыunion
и обсуждающий вопросы, связанные с поведением, определяемым реализацией последнего примера выше