Тип каламбур
В этой статье есть несколько проблем. Пожалуйста, помогите улучшить его или обсудите эти проблемы на странице обсуждения . ( Узнайте, как и когда удалять эти шаблонные сообщения )
|
В информатике — каламбур типов это любой метод программирования, который подрывает или обходит систему типов языка программирования для достижения эффекта, которого было бы трудно или невозможно достичь в рамках формального языка.
В C и C++ такие конструкции, как указателей преобразование типов и union
— В C++ добавлено ссылочного типа и преобразование reinterpret_cast
в этот список — предназначены для того, чтобы разрешить многие виды каламбура типов, хотя некоторые виды фактически не поддерживаются стандартным языком.
В языке программирования Паскаль использование записей с вариантами может использоваться для обработки определенного типа данных более чем одним способом или способом, который обычно не разрешен.
Пример сокетов
[ редактировать ]Классический пример игры слов типов можно найти в интерфейсе сокетов Беркли . Функция привязки открытого, но неинициализированного сокета к IP-адресу объявлена следующим образом:
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
The bind
функция обычно вызывается следующим образом:
struct sockaddr_in sa = {0};
int sockfd = ...;
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
bind(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 , быстрое сравнение FP как целых чисел, [1] и нахождение соседних значений путем увеличения как целого числа (реализация nextafter
). [2]
По языку
[ редактировать ]С и С++
[ редактировать ]В дополнение к предположению о битовом представлении чисел с плавающей запятой, приведенный выше пример каламбура типов с плавающей запятой также нарушает ограничения языка C на способ доступа к объектам: [3] заявленный тип x
является float
но оно читается через выражение типа unsigned int
. На многих распространенных платформах такое использование каламбура указателей может создать проблемы, если разные указатели выравниваются специфическими для машины способами . Более того, указатели разных размеров могут совпадать с доступом к одной и той же памяти , вызывая проблемы, которые не контролируются компилятором. Однако даже если размер данных и представление указателя совпадают, компиляторы могут полагаться на ограничения отсутствия псевдонимов для выполнения оптимизаций, которые были бы небезопасны при наличии запрещенного псевдонимов.
Использование указателей
[ редактировать ]Наивную попытку каламбура можно осуществить с помощью указателей: (Следующий пример предполагает битовое представление типа IEEE-754). float
.)
bool is_negative(float x) {
int32_t i = *(int32_t*)&x; // In C++ this is equivalent to: 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] однако, если только последний записанный член union
считается, что оно вообще имеет какую-либо ценность.
Другой пример каламбура см. в разделе «Шаг массива» .
Использование bit_cast
[ редактировать ] В C++ 20 std::bit_cast
функция позволяет каламбур типов без неопределенного поведения. Это также позволяет пометить функцию constexpr
.
constexpr bool is_negative(float x) noexcept {
static_assert(std::numeric_limits<float>::is_iec559); // (enable only on IEEE 754)
auto i = std::bit_cast<std::int32_t>(x);
return i < 0 ;
}
Паскаль
[ редактировать ]Запись варианта позволяет рассматривать тип данных как несколько типов данных в зависимости от того, на какой вариант ссылаются. В следующем примере , что целое число предполагается имеет длину 16 бит, в то время как longint и вещественное число считаются равными 32, а символ — 8 бит:
type
VariantRecord = record
case RecType : LongInt of
1: (I : array[1..2] of Integer); (* not show here: there can be several variables in a variant record's case statement *)
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]; (* this would extract the first byte of V.I *)
V.R := 8.3;
LA := V.L; (* this would store a Real into an Integer *)
В Паскале копирование вещественного числа в целое число преобразует его в усеченное значение. Этот метод преобразует двоичное значение числа с плавающей запятой в любое длинное целое число (32 бита), которое не будет одинаковым и может быть несовместимо со значением длинного целого числа в некоторых системах.
Эти примеры могут быть использованы для создания странных преобразований, хотя в некоторых случаях эти типы конструкций могут быть законно использованы, например, для определения местоположения определенных фрагментов данных. В следующем примере предполагается, что указатель и longint являются 32-битными:
type
PA = ^Arec;
Arec = record
case RT : LongInt of
1: (P : PA );
2: (L : LongInt);
end;
var
PP : PA;
K : LongInt;
New(PP);
PP^.P := PP;
WriteLn('Variable PP is located at address ', Hex(PP^.L));
Где «new» — это стандартная процедура в Паскале для выделения памяти для указателя, а «hex» — это предположительно процедура печати шестнадцатеричной строки, описывающей значение целого числа. Это позволит отображать адрес указателя, что обычно не допускается. (Указатели нельзя читать или записывать, их можно только присваивать.) Присвоение значения целочисленному варианту указателя позволит проверять или записывать любое место в системной памяти:
PP^.L := 0;
PP := PP^.P; (* PP now points to address 0 *)
K := PP^.L; (* K contains the value of word 0 *)
WriteLn('Word 0 of this machine contains ', K);
Эта конструкция может вызвать проверку программы или нарушение защиты, если адрес 0 защищен от чтения на машине, на которой запущена программа, или в операционной системе, под которой она работает.
Техника переосмысления приведения из C/C++ также работает в Паскале. Это может быть полезно, например. чтение dwords из потока байтов, и мы хотим рассматривать их как числа с плавающей запятой. Вот рабочий пример, в котором мы переосмысливаем преобразование двойного слова в число с плавающей запятой:
type
pReal = ^Real;
var
DW : DWord;
F : Real;
F := pReal(@DW)^;
С#
[ редактировать ]В C# (и других языках .NET) каламбур типов реализовать немного сложнее из-за системы типов, но, тем не менее, это можно сделать с помощью указателей или объединений структур.
Указатели
[ редактировать ]C# допускает указатели только на так называемые собственные типы, т. е. на любой примитивный тип (кроме string
), перечисление, массив или структура, состоящая только из других собственных типов. Обратите внимание, что указатели разрешены только в блоках кода, помеченных как «небезопасные».
float pi = 3.14159;
uint piAsRawData = *(uint*)π
Структурные союзы
[ редактировать ]Объединения структур разрешены без какого-либо понятия «небезопасного» кода, но они требуют определения нового типа.
[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;
Необработанный CIL-код
[ редактировать ]необработанный CIL Вместо C# можно использовать , поскольку он не имеет большинства ограничений типов. Это позволяет, например, объединить два значения перечисления универсального типа:
TEnum a = ...;
TEnum b = ...;
TEnum combined = a | b; // illegal
Это можно обойти с помощью следующего кода CIL:
.method public static hidebysig
!!TEnum CombineEnums<valuetype .ctor ([mscorlib]System.ValueType) TEnum>(
!!TEnum a,
!!TEnum b
) cil managed
{
.maxstack 2
ldarg.0
ldarg.1
or // this will not cause an overflow, because a and b have the same type, and therefore the same size.
ret
}
The cpblk
Код операции CIL допускает некоторые другие трюки, например преобразование структуры в массив байтов:
.method public static hidebysig
uint8[] ToByteArray<valuetype .ctor ([mscorlib]System.ValueType) T>(
!!T& v // 'ref T' in C#
) cil managed
{
.locals init (
[0] uint8[]
)
.maxstack 3
// create a new byte array with length sizeof(T) and store it in local 0
sizeof !!T
newarr uint8
dup // keep a copy on the stack for later (1)
stloc.0
ldc.i4.0
ldelema uint8
// memcpy(local 0, &v, sizeof(T));
// <the array is still on the stack, see (1)>
ldarg.0 // this is the *address* of 'v', because its type is '!!T&'
sizeof !!T
cpblk
ldloc.0
ret
}
Ссылки
[ редактировать ]- ^ Херф, Майкл (декабрь 2001 г.). «радикс-трюки» . стереопсис: графика .
- ^ «Глупые трюки с плаванием» . Случайный ASCII — технический блог Брюса Доусона . 24 января 2012 г.
- ^ ИСО/МЭК 9899:1999, раздел 6.5/7.
- ^ «§ 6.5/7» (PDF) , ISO/IEC 9899:2018 , 2018, стр. 55, заархивировано из оригинала (PDF) 30 декабря 2018 г.
Доступ к сохраненному значению объекта должен осуществляться только с помощью выражения lvalue, которое имеет один из следующих типов: [...]
- ^ «Ошибки GCC — проект GNU» . gcc.gnu.org .
- ^ «§ 6.5.2.3/3, сноска 97» (PDF) , ISO/IEC 9899:2018 , 2018, стр. 59, заархивировано из оригинала (PDF) 30 декабря 2018 г.
Если член, используемый для чтения содержимого объекта объединения, не совпадает с элементом, который последний раз использовался для хранения значения в объекте, соответствующая часть объектное представление значения переинтерпретируется как объектное представление в новом типе, как описано в 6.2.6 ( процесс, который иногда называют «каламбуром типа» ). Это может быть представление-ловушка.
- ^
«§ J.1/1, пункт 11» (PDF) , ISO/IEC 9899:2018 , 2018, стр. 403, заархивировано из оригинала (PDF) 30 декабря 2018 г.
Следующие не указаны:… Значения байтов, которые соответствуют членам объединения, отличным от того, который последним был сохранен в (6.2.6.1).
- ^ ISO/IEC 14882:2011, раздел 9.5.
Внешние ссылки
[ редактировать ]- Раздел GCC руководства по
-fstrict-aliasing
, что побеждает некоторые каламбуры - Отчет о дефектах 257 в соответствии со стандартом C99 , в котором, кстати, определяется «каламбур типов» с точки зрения
union
и обсуждение проблем, связанных с поведением, определяемым реализацией последнего примера выше. - Отчет о дефекте 283, связанный с использованием союзов для каламбура типов.