В информатике , типа каламбурная является общим термином для любой техники программирования , который ниспровергает или обходит тип операционной системы на более языка программирования для того , чтобы достичь эффекта , который будет трудно или невозможно достичь в рамках формального языка.
В C и C ++ такие конструкции, как преобразование типа указателя и - C ++ добавляет преобразование ссылочного типа и в этот список - предоставляются для того, чтобы разрешить многие виды каламбура типов, хотя некоторые виды фактически не поддерживаются стандартным языком.union
reinterpret_cast
В языке программирования Паскаль использование записей с вариантами может использоваться для обработки определенного типа данных более чем одним способом или способом, который обычно не разрешен.
Пример сокетов
Один классический пример каламбура можно найти в интерфейсе сокетов Беркли . Функция привязки открытого, но неинициализированного сокета к IP-адресу объявляется следующим образом:
int bind ( int sockfd , struct sockaddr * my_addr , socklen_t addrlen );
bind
Функция обычно называется следующим образом :
struct sockaddr_in sa = { 0 }; int 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
is of type struct sockaddr*
) фактически будет относиться к полю sa.sin_family
(где sa
is of type struct sockaddr_in
). Другими словами, библиотека сокетов использует каламбур для реализации элементарной формы полиморфизма или наследования .
В мире программирования часто встречается использование «дополненных» структур данных, позволяющих хранить различные типы значений в том же самом пространстве хранения. Это часто наблюдается, когда две структуры используются во взаимной исключительности для оптимизации.
Пример с плавающей точкой
Не во всех примерах каламбура используются структуры, как в предыдущем примере. Предположим, мы хотим определить, является ли число с плавающей запятой отрицательным. Мы могли бы написать:
bool is_negative ( float x ) { return x < 0,0 ; }
Однако, предположив, что сравнения с плавающей запятой являются дорогостоящими, а также предположим, что float
это представлено в соответствии со стандартом с плавающей запятой IEEE , а целые числа имеют ширину 32 бита, мы могли бы использовать каламбур для извлечения знакового бита числа с плавающей запятой. используя только целочисленные операции:
BOOL is_negative ( флоат х ) { беззнаковое INT * щ = ( беззнаковое INT * ) & х ; возврат * ui & 0x80000000 ; }
Обратите внимание , что поведение не будет точно таким же: в частном случае x
являются отрицательным нуль , первыми выходами реализации в false
то время как второй урожайность true
. Кроме того, первая реализация вернет false
любое значение NaN , но последнее может вернуться true
для значений NaN с установленным битом знака.
Этот вид каламбура более опасен, чем большинство других. В то время как первый пример полагался только на гарантии, сделанные языком программирования C в отношении компоновки структуры и конвертируемости указателей, последний пример основывается на предположениях об аппаратном обеспечении конкретной системы. В некоторых ситуациях, таких как критичный по времени код, который компилятор не может оптимизировать в противном случае , может потребоваться опасный код. В этих случаях документирование всех таких предположений в комментариях и введение статических утверждений для проверки ожиданий переносимости помогает сохранить поддерживаемый код .
Практические примеры каламбура с плавающей запятой включают быстрый обратный квадратный корень, популяризированный Quake III , быстрое сравнение FP как целые числа [1] и поиск соседних значений путем увеличения как целого числа (реализация nextafter
). [2]
По языку
C и C ++
В дополнение к предположению о битовом представлении чисел с плавающей запятой, приведенный выше пример выбора типа с плавающей запятой также нарушает ограничения языка C на способ доступа к объектам: [3] объявленный тип x
is, float
но он читается через выражение типа unsigned int
. На многих распространенных платформах такое использование перфорации указателей может создать проблемы, если разные указатели выровнены машинно-зависимыми способами . Более того, указатели разного размера могут использовать псевдонимы доступа к одной и той же памяти , вызывая проблемы, которые компилятор не проверяет.
Использование указателей
Наивная попытка набора текста может быть достигнута с помощью указателей:
float pi = 3,14159 ; uint32_t piAsRawData = * ( uint32_t * ) & pi ;
Согласно стандарту C этот код не должен (или, скорее, не должен) компилироваться, однако, если он это делает, то piAsRawData
обычно содержит необработанные биты числа пи.
Использование union
Попытки исправить каламбур с помощью расширения - распространенная ошибка union
. (В следующем примере дополнительно делается предположение о битовом представлении IEEE-754 для типов с плавающей запятой).
bool is_negative ( float x ) { объединение { unsigned int ui ; float d ; } my_union = { . d = x }; вернуть my_union . ui & 0x80000000 ; }
Доступ my_union.ui
после инициализации другого члена, my_union.d
по-прежнему является формой каламбура [4] в C, и результатом является неопределенное поведение [5] (и неопределенное поведение в C ++ [6] ).
Язык § 6.5 / 7 [3] может быть неправильно истолкован, чтобы подразумевать, что чтение альтернативных членов союза допустимо. Однако текст гласит: «Сохраненное значение объекта должно быть доступно только …». Это ограничивающее выражение, а не утверждение, что все возможные члены объединения могут быть доступны независимо от того, какой из них был сохранен последним. Таким образом, использование не union
позволяет избежать проблем, связанных с простым перетаскиванием указателя напрямую.
Его можно даже считать менее безопасным, чем перенаправление типов с использованием указателей, поскольку компилятор с меньшей вероятностью сообщит о предупреждении или ошибке, если он не поддерживает перенаправление типов.
Компиляторы, такие как GCC, поддерживают доступ к псевдонимам, как в приведенных выше примерах, в качестве расширения языка. [7] На компиляторах без такого расширения правило строгого псевдонима нарушается только явным memcpy или использованием указателя char в качестве «среднего человека» (поскольку они могут иметь произвольный псевдоним).
Другой пример каламбура см. В разделе « Шаг массива» .
Паскаль
Запись варианта позволяет обрабатывать тип данных как несколько видов данных в зависимости от того, на какой вариант ссылается. В следующем примере предполагается , что целое число является 16-битным, в то время как longint и вещественное число - 32, а символ - 8-битным:
type VariantRecord = запись case RecType : LongInt of 1 : ( I : array [ 1 .. 2 ] of Integer ) ; (* не показывать здесь: в операторе case вариантной записи может быть несколько переменных *) 2 : ( L : LongInt ) ; 3 : ( R : Реальный ) ; 4 : ( C : массив [ 1 .. 4 ] из Char ) ; конец ;var V : VariantRecord ; K : целое число ; ЛА : LongInt ; РА : Настоящее ; Ch : Персонаж ;В . I [ 1 ] : = 1 ; Ч. : = V . C [ 1 ] ; (* Это было бы извлечь первые байты VI *) V . R : = 8,3 ; Л. : = V . L ; (* это сохранит вещественное число в целое число *)
В Паскале копирование действительного числа в целое число преобразует его в усеченное значение. Этот метод преобразует двоичное значение числа с плавающей запятой во что бы то ни было в виде длинного целого числа (32 бита), которое не будет таким же и может быть несовместимо со значением длинного целого числа в некоторых системах.
Эти примеры можно использовать для создания странных преобразований, хотя в некоторых случаях эти типы конструкций могут иметь законное использование, например, для определения местоположения определенных фрагментов данных. В следующем примере предполагается, что указатель и longint являются 32-битными:
введите PA = ^ Arec ; ARec = запись случай RT : LongInt от 1 : ( Р : ПА ) ; 2 : ( L : LongInt ) ; конец ;вар ПП : ПА ; K : LongInt ;Новый ( ПП ) ; PP ^. P : = PP ; WriteLn ( 'Переменная PP находится по адресу' , Hex ( PP ^. L )) ;
Где «новый» - это стандартная процедура в Паскале для выделения памяти для указателя, а «шестнадцатеричный» - предположительно процедура для печати шестнадцатеричной строки, описывающей значение целого числа. Это позволит отображать адрес указателя, что обычно недопустимо. (Указатели не могут быть прочитаны или записаны, их можно только присвоить.) Присвоение значения целочисленному варианту указателя позволит исследовать или записывать в любое место в системной памяти:
PP ^. L : = 0 ; ПП : = ПП ^. P ; (* PP теперь указывает на адрес 0 *) K : = PP ^. L ; (* K содержит значение слова 0 *) WriteLn ( 'Слово 0 этой машины содержит' , K ) ;
Эта конструкция может вызвать проверку программы или нарушение защиты, если адрес 0 защищен от чтения на машине, на которой выполняется программа, или в операционной системе, в которой она работает.
Техника переинтерпретирования приведения из C / C ++ также работает в Паскале. Это может быть полезно, например, когда. чтение двойных слов из байтового потока, и мы хотим рассматривать их как плавающие. Вот рабочий пример, в котором мы переосмысливаем преобразование двойного слова в поплавок:
введите pReal = ^ Real ;var DW : DWord ; F : Реальный ;F : = pReal ( @ DW ) ^;
C #
В C # (и других языках .NET) каламбура типов немного сложнее достичь из-за системы типов, но тем не менее это можно сделать с помощью указателей или структурных объединений.
Указатели
C # допускает указатели только на так называемые собственные типы, то есть любой примитивный тип (кроме string
), перечисление, массив или структуру, которые состоят только из других собственных типов. Обратите внимание, что указатели разрешены только в блоках кода, помеченных как «небезопасные».
float pi = 3,14159 ; uint piAsRawData = * ( uint *) & pi ;
Структурные союзы
Объединения структур разрешены без какого-либо понятия «небезопасный» код, но они требуют определения нового типа.
[StructLayout (LayoutKind.Explicit)] struct FloatAndUIntUnion { [FieldOffset (0)] public float DataAsFloat ; [FieldOffset (0)] общедоступный uint DataAsUInt ; }// ...FloatAndUIntUnion union ; союз . DataAsFloat = 3,14159 ; uint piAsRawData = union . DataAsUInt ;
Необработанный код CIL
Raw CIL можно использовать вместо C #, потому что он не имеет большинства ограничений типа. Это позволяет, например, объединить два значения перечисления универсального типа:
TEnum a = ...; TEnum b = ...; TEnum комбинированный = a | б ; // незаконно
Это можно обойти с помощью следующего кода CIL:
. метод public static hidebysig !! TEnum CombineEnums < valuetype . ctor ([ mscorlib ] System . ValueType ) TEnum > ( !! TEnum a , !! TEnum b ) cil managed { . maxstack 2 ldarg . 0 ldarg . 1 или // это не вызовет переполнения, потому что a и b имеют один и тот же тип и, следовательно, одинаковый размер. ret }
Код cpblk
операции CIL позволяет использовать некоторые другие приемы, такие как преобразование структуры в массив байтов:
. метод public static hidebysig uint8 [] ToByteArray < valuetype . ctor ([ mscorlib ] System . ValueType ) T > ( !! T & v // 'ref T' в C # ) cil managed { . locals init ( [0] uint8 [] ) . maxstack 3 // создаем новый массив байтов с длиной sizeof (T) и сохраняем его в локальном 0 sizeof !! T newarr uint8 dup // сохраняем копию в стеке на будущее (1) stloc . 0 ldc . i4 . 0 ldelema uint8 // memcpy (локальный 0, & v, sizeof (T)); // <массив все еще в стеке, см. (1)> ldarg . 0 // это * адрес * 'v', потому что его тип '!! T &' sizeof !! T cpblk ldloc . 0 ret }
Рекомендации
- ^ Херф, Майкл (декабрь 2001). "хитрости счисления" . стереопсис: графика .
- ^ «Глупые трюки с плавающей запятой» . Случайный ASCII - технический блог Брюса Доусона . 24 января 2012 г.
- ^ a b ISO / IEC 9899: 1999 s6.5 / 7
- ^ «§ 6.5.2.3/3, сноска 97», ISO / IEC 9899: 2018 (PDF) , 2018, стр. 59, заархивировано из исходного (PDF) 30 декабря 2018 г.
Если член, используемый для чтения содержимого объекта объединения, не совпадает с элементом, последним использовавшимся для хранения значения в объекте, соответствующая часть объектное представление значения переинтерпретируется как объектное представление в новом типе, как описано в 6.2.6 ( процесс, иногда называемый «каламбуром типов» ). Это может быть ловушка.
- ^ «§ J.1 / 1, bullet 11», ISO / IEC 9899: 2018 (PDF) , 2018, стр. 403, заархивировано из оригинала (PDF) 30 декабря 2018
г. Следующее не определено:… Значения байтов, которые соответствуют членам объединения, кроме последнего, сохраненного в (6.2.6.1).
- ^ ISO / IEC 14882: 2011 Раздел 9.5
- ^ GCC: не содержит ошибок
Внешние ссылки
- Раздел руководства GCC по -fstrict-aliasing , который побеждает некоторые типы каламбура
- Отчет о дефектах 257 в соответствии со стандартом C99 , в котором случайно определяется "каламбур типов" в терминах
union
и обсуждаются проблемы, связанные с поведением, определяемым реализацией, в последнем примере выше. - Отчет о дефектах 283 об использовании объединений для набора текста