stringtranslate.com

Тип каламбур

В информатике каламбур типов — это любой метод программирования, который подрывает или обходит систему типов языка программирования для достижения эффекта, которого было бы трудно или невозможно достичь в рамках формального языка.

В C и C++ такие конструкции, как преобразование типов указателей и — C++ добавляет преобразование ссылочных типов и в этот список — предусмотрены для того, чтобы разрешить многие виды каламбура типов, хотя некоторые виды фактически не поддерживаются стандартным языком.unionreinterpret_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 = объединение . ДанныеАсуинт ;      

Необработанный CIL-код

Вместо 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 снятие } 

Рекомендации

  1. ^ Херф, Майкл (декабрь 2001 г.). «радиксные трюки». стереопсис: графика .
  2. ^ "Глупые трюки с плаванием" . Случайный ASCII — технический блог Брюса Доусона . 24 января 2012 г.
  3. ^ ИСО/МЭК 9899:1999, раздел 6.5/7.
  4. ^ «§ 6.5/7» (PDF) , ISO/IEC 9899:2018 , 2018, стр. 55, заархивировано из оригинала (PDF) 30 декабря 2018 г. Доступ к сохраненному значению объекта должен осуществляться только с помощью выражения lvalue, которое имеет один из следующих типов: [...]
  5. ^ "Ошибки GCC - Проект GNU" . gcc.gnu.org .
  6. ^ «§ 6.5.2.3/3, сноска 97» (PDF) , ISO/IEC 9899:2018 , 2018, стр. 59, заархивировано из оригинала (PDF) 30 декабря 2018 г. Если член, используемый для чтения содержимого объекта объединения, не совпадает с элементом, который последний раз использовался для хранения значения в объекте, соответствующая часть объектное представление значения переинтерпретируется как объектное представление в новом типе, как описано в 6.2.6 ( процесс, который иногда называют «каламбуром типа» ). Это может быть представление-ловушка.
  7. ^ «§ J.1/1, пункт 11» (PDF) , ISO/IEC 9899:2018 , 2018, стр. 403, заархивировано из оригинала (PDF) 30 декабря 2018 г. Следующие не указаны:… Значения байтов, которые соответствуют членам объединения, отличным от того, который последним был сохранен в (6.2.6.1).
  8. ^ ISO/IEC 14882:2011, раздел 9.5.

Внешние ссылки