В информатике каламбур типов — это любой метод программирования, который подрывает или обходит систему типов языка программирования для достижения эффекта, которого было бы трудно или невозможно достичь в рамках формального языка.
В C и C++ такие конструкции, как преобразование типов указателей и — C++ добавляет преобразование ссылочных типов и в этот список — предусмотрены для того, чтобы разрешить многие виды каламбура типов, хотя некоторые виды фактически не поддерживаются стандартным языком.union
reinterpret_cast
В языке программирования Паскаль использование записей с вариантами может использоваться для обработки определенного типа данных более чем одним способом или способом, который обычно не разрешен.
Классический пример каламбура типов можно найти в интерфейсе сокетов Беркли . Функция привязки открытого, но неинициализированного сокета к IP-адресу объявлена следующим образом:
intbind ( 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 ; вернуть * я < 0 ; }
Обратите внимание, что поведение не будет точно таким же: в особом случае x
отрицательного нуля первая реализация дает результат false
, а вторая — true
. Кроме того, первая реализация возвращает false
любое значение NaN , а вторая может возвращать true
значения NaN с установленным знаковым битом.
Этот вид каламбура более опасен, чем большинство других. В то время как первый пример основывался только на гарантиях языка программирования C относительно структуры структуры и конвертируемости указателей, второй пример основан на предположениях об аппаратном обеспечении конкретной системы. В некоторых ситуациях, например код, критичный по времени , который компилятор иначе не может оптимизировать , может потребоваться опасный код. В этих случаях документирование всех подобных предположений в комментариях и введение статических утверждений для проверки ожиданий переносимости помогает сохранить код поддерживаемым .
Практические примеры каламбура с плавающей запятой включают быстрое обратное квадратное корень , популяризированное Quake III , быстрое сравнение FP как целых чисел [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); вернуть я < 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 ; плавать д ; } мой_союз ; мой_союз . д знак равно х ; верните my_union . я < 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 ) noException { static_assert ( std :: numeric_limits < float >:: is_iec559 ); // (включено только в IEEE 754) auto i = std :: bit_cast < std :: int32_t > ( x ); вернуть я < 0 ; }
Запись варианта позволяет рассматривать тип данных как несколько типов данных в зависимости от того, на какой вариант ссылаются. В следующем примере предполагается, что целое число составляет 16 бит, в то время как longint и вещественное число считаются 32-битными, а символ — 8-битным:
тип VariantRecord = регистр записи RecType : LongInt of 1 : ( I : массив [ 1..2 ] целого числа ) ; (* здесь не показано: в операторе варианта записи варианта может быть несколько переменных *) 2 : ( L : LongInt ) ; 3 : ( Р : Реальный ) ; 4 : ( C : массив [ 1..4 ] из Char ) ; конец ; вар V : ВариантЗапись ; К : Целое число ; ЛА : ЛонгИнт ; РА : Реальный ; Ч : Характер ; В. Я [ 1 ] := 1 ; Ч := В . С [ 1 ] ; (* это позволит извлечь первый байт VI *) V . Р := 8,3 ; ЛА := В . Л ; (* это сохранит вещественное число в целое число *)
В Паскале копирование вещественного числа в целое число преобразует его в усеченное значение. Этот метод преобразует двоичное значение числа с плавающей запятой в любое длинное целое число (32 бита), которое не будет одинаковым и может быть несовместимо со значением длинного целого числа в некоторых системах.
Эти примеры могут быть использованы для создания странных преобразований, хотя в некоторых случаях эти типы конструкций могут быть законно использованы, например, для определения местоположения определенных фрагментов данных. В следующем примере предполагается, что указатель и longint являются 32-битными:
тип PA = ^ Arec ; Arec = регистр записи RT : LongInt of 1 : ( P : PA ) ; 2 : ( L : LongInt ) ; конец ; вар ПП : ПА ; К : ЛонгИнт ; Новый ( ПП ) ; ПП ^. П := ПП ; WriteLn ( 'Переменная PP находится по адресу' , Hex ( PP ^.L ) ) ;
Где «new» — это стандартная процедура в Паскале для выделения памяти для указателя, а «hex» — это предположительно процедура печати шестнадцатеричной строки, описывающей значение целого числа. Это позволит отображать адрес указателя, что обычно не допускается. (Указатели нельзя читать или записывать, их можно только присваивать.) Присвоение значения целочисленному варианту указателя позволит проверять или записывать любое место в системной памяти:
ПП ^. Л := 0 ; ПП := ПП ^. П ; (*PP теперь указывает на адрес 0 *) K := PP ^. Л ; (* K содержит значение слова 0 *) WriteLn ( 'Слово 0 этой машины содержит ' , K ) ;
Эта конструкция может вызвать проверку программы или нарушение защиты, если адрес 0 защищен от чтения на машине, на которой запущена программа, или в операционной системе, под которой она работает.
Техника переосмысления приведения из C/C++ также работает в Паскале. Это может быть полезно, например. чтение dwords из потока байтов, и мы хотим рассматривать их как числа с плавающей запятой. Вот рабочий пример, в котором мы переосмысливаем преобразование двойного слова в число с плавающей запятой:
введите pReal = ^ Real ; вар DW : DWord ; Ф : Реальный ; F := pReal ( @DW ) ^ ;
В C# (и других языках .NET) каламбур типов реализовать немного сложнее из-за системы типов, но, тем не менее, это можно сделать с помощью указателей или объединений структур.
В C# разрешены указатели только на так называемые собственные типы, т. е. на любой примитивный тип (кроме string
), перечисление, массив или структуру, состоящую только из других собственных типов. Обратите внимание, что указатели разрешены только в блоках кода, помеченных как «небезопасные».
плавающее число пи = 3,14159 ; uint piAsRawData = * ( uint * ) & pi ;
Объединения структур разрешены без какого-либо понятия «небезопасного» кода, но они требуют определения нового типа.
[StructLayout(LayoutKind.Explicit)] struct FloatAndUIntUnion { [FieldOffset(0)] public float DataAsFloat ; [FieldOffset(0)] public uint DataAsUInt ; } // ...FloatAndUIntUnion объединение ; союз . DataAsFloat = 3.14159 ; uint piAsRawData = объединение . ДанныеАсуинт ;
Вместо C# можно использовать необработанный CIL , поскольку он не имеет большинства ограничений типов. Это позволяет, например, объединить два значения перечисления универсального типа:
TEnum а = ...; TEnum b = ...; TEnum комбинированный = a | б ; // незаконно
Это можно обойти с помощью следующего кода CIL:
. метод public staticidebysig !! TEnum JointEnums < тип значения . ctor ([ mscorlib ] System . ValueType ) TEnum > ( !! TEnum a , !! TEnum b ) cil Managed { . Максстек 2 лдарг . 0 лдарг . 1 или // это не приведет к переполнению, поскольку a и b имеют один и тот же тип и, следовательно, одинаковый размер. в отставку }
Код cpblk
операции CIL допускает некоторые другие трюки, например преобразование структуры в массив байтов:
. метод public staticidebysig uint8 [] ToByteArray < тип значения . ctor ([ mscorlib ] System . ValueType ) T > ( !! T & v // 'ref T' в C# ) cil Managed { . местные жители init ( [0] uint8 [] ) . Максстек 3 // создаем новый массив байтов длиной sizeof(T) и сохраняем его в локальном 0 sizeof !! T newarr uint8 dup // сохраняем копию в стеке на будущее (1) stloc . 0 ООО . я4 . 0 лделема uint8 // memcpy(local 0, &v, sizeof(T)); // <массив все еще находится в стеке, см. (1)> ldarg . 0 // это *адрес* 'v', потому что его тип '!!T&' sizeof !! Т cpblk лдлок . 0 снятие }
Доступ к сохраненному значению объекта должен осуществляться только с помощью выражения lvalue, которое имеет один из следующих типов: [...]
Если член, используемый для чтения содержимого объекта объединения, не совпадает с элементом, который последний раз использовался для хранения значения в объекте, соответствующая часть объектное представление значения переинтерпретируется как объектное представление в новом типе, как описано в 6.2.6 (
процесс, который иногда называют «каламбуром типа»
).
Это может быть представление-ловушка.
Следующие не указаны:… Значения байтов, которые соответствуют членам объединения,
отличным от того, который последним был сохранен в
(6.2.6.1).
-fstrict-aliasing
, который устраняет некоторые каламбуры.union
, и обсуждающий проблемы, связанные с поведением, определяемым реализацией последнего примера выше.