Каламбур
- 1 year ago
- 0
- 0
Каламбур типизации ( англ. type punning ) — термин, который используется в информатике для обозначения различных техник нарушения или обмана системы типов некоторого языка программирования , имеющих эффект, который было бы затруднительно или невозможно обеспечить в рамках формального языка .
Языки
Си
и
C++
предоставляют явные возможности каламбура
типизации
посредством таких конструкций, как
приведение типов
,
union
, а также
reinterpret_cast
для
C++
, хотя стандарты этих языков некоторые случаи таких каламбуров трактуют, как
неопределённое поведение
.
В языке Pascal записи с вариантами могут использоваться для интерпретации конкретного типа данных более, чем одним способом, или даже не предусмотренным языком способом.
Каламбур типизации является прямым нарушением
типобезопасности
. Традиционно возможность построить каламбур типизации связывается со
слабой типизацией
, но и некоторые сильно типизированные языки или их реализации предоставляют такие возможности (как правило, используя в связанных с ними идентификаторах слова
unsafe
или
unchecked
). Сторонники типобезопасности утверждают, что «
необходимость
» каламбуров типизации является мифом
.
JS позволяет неявное приведение типов между строками и числами, что может приводить к нелогичным результатам, например:
console.log(2 + 2) // 4
console.log("2" + "2") // "22"
console.log(2 + 2 - 2) // 2
console.log("2" + "2" - "2") // "20"
Оператор
+
для чисел работает как сложение, а для строк как конкатенация, однако оператор
-
работает только как вычитание для чисел, поэтому в последнем выражении мы получаем
"22" - "2"
что приводит к значению
20
.
Сравнение между значениями разных типов в JS не транзитивно:
0 == "0"
0 == []
"0" != []
Классический пример каламбура типизации можно видеть в интерфейсе сокетов Беркли . Функция , которая связывает открытый неинициализированный сокет с IP-адресом , имеет такую сигнатуру:
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
Функция
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);
Библиотека
сокетов Беркли
в своей основе опирается на тот факт, что в языке
Си
указатель
на
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.0;
}
Однако, сравнения над числами с плавающей запятой являются ресурсоёмкими, так как действуют особым образом для
NaN
. Приняв во внимание, что тип
float
представлен согласно стандарту
IEEE 754-2008
, а тип
int
имеет размер 32
бита
и за
знак
в нём отвечает тот же
бит
, что и в
float
, можно применить каламбур типизации для извлечения
бита знака
числа с плавающей запятой, используя только целочисленное сравнение:
bool is_negative(float x) {
return *((int*)&x) < 0;
}
Такая форма каламбура типизации является наиболее опасной. Предыдущий пример опирался только на гарантии, данные языком Си в отношении представления структур и преобразуемости указателей ; однако, данный пример опирается на предположения в отношении конкретного аппаратного обеспечения . В некоторых случаях, например, при разработке приложений реального времени , которые компилятор не способен оптимизировать самостоятельно, такие опасные программные решения оказываются необходимыми. В таких случаях обеспечить поддерживаемость кода помогают комментарии и проверки времени компиляции ( англ. ).
Реальный пример можно найти в коде Quake III — см. Быстрый обратный квадратный корень .
В дополнение к предположениям о
битовом
представлении
чисел с плавающей запятой
вышеприведённый пример каламбура типизации также нарушает установленные
языком Си
правила доступа к
объектам
:
x
объявлен как
float
, но его значение считывается в выражении, имеющем
тип
signed int
. На многих распространённых платформах такой каламбур типизации
указателей
может привести к проблемам, если
указатели
различным образом
выровнены в памяти
. Более того,
указатели
разного размера могут осуществлять
, приводя к
ошибкам
, которые не могут быть обнаружены
компилятором
.
Проблема
может быть решена посредством использования
union
(хотя пример ниже основывается на предположении, что
число с плавающей запятой
представлено по стандарту
IEEE-754
):
bool is_negative(float x) {
union {
unsigned int ui;
float d;
} my_union = { .d = x };
return (my_union.ui & 0x80000000) != 0;
}
Это код на C99 с использованием обозначенных инициализаторов ( англ. Designated initialisers ). При создании объединения инициализируется его вещественное поле, а затем происходит чтение значения целого поля (физически размещенного по тому же адресу в памяти), согласно пункту s6.5 стандарта. Некоторые компиляторы поддерживают такие конструкции в качестве расширения языка — например, GCC .
В качестве ещё одного примера каламбура типизации см. (англ.) .
Вариантная
запись
позволяет рассматривать
тип данных
различным образом в зависимости от указанного варианта. В следующем примере предполагается, что
integer
имеет размер 16 бит,
longint
и
real
— 32 бита, а
character
— 8 бит:
type variant_record = record
case rec_type : longint of
1: ( I : array [1..2] of integer );
2: ( L : longint );
3: ( R : real );
4: ( C : array [1..4] of character );
end;
Var V: Variant_record;
K: Integer;
LA: Longint;
RA: Real;
Ch: character;
...
V.I := 1;
Ch := V.C[1]; (* Получаем первый байт поля V.I *)
V.R := 8.3;
LA := V.L; (* Сохраняем вещественное число в целочисленную ячейку *)
В Паскале копирование вещественного в целое преобразует его в округлённое значение. Данный же метод преобразует двоичное значение числа с плавающей запятой в нечто, имеющее длину длинного целого (32 бита), что не тождественно и даже может быть несовместимо с длинными целыми на некоторых платформах.
Подобные примеры могут использоваться для странных преобразований, однако, в некоторых случаях такие конструкции могут иметь смысл, например, вычисление местоположения определённых фрагментов данных. В следующем примере предполагается, что указатель и длинное целое имеют размер 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('Переменная PP размещена в памяти по адресу ', hex(PP^.L));
Стандартная процедура
New
в Паскале предназначена для динамического выделения памяти для указателя, а
hex
подразумевается некой процедурой, печатающей шестнадцатиричную строку, описывающую значение целого. Это позволяет вывести на экран адрес указателя, что обычно запрещено (указатели в Паскале нельзя читать или выводить — только присваивать). Присваивание значения целому варианту указателя позволяет читать и изменять любой участок системной памяти:
PP^.L := 0;
PP := PP^.P; (* PP указывает на адрес 0 *)
K := PP^.L; (* K содержит значение слова по адресу 0 *)
Writeln(' Слово по адресу 0 данной машины содержит ', K);
Эта программа может работать корректно или обрушиться , если адрес 0 защищён от чтения, в зависимости от операционной системы.
union
и обсуждающий зависящее от реализации поведение вышеприведённого кода
union
для каламбуров типизации