Каламбур типизацииКаламбур типизации (англ. type punning) — термин, который используется в информатике для обозначения различных техник нарушения или обмана системы типов некоторого языка программирования, имеющих эффект, который было бы затруднительно или невозможно обеспечить в рамках формального языка. Языки Си и C++ предоставляют явные возможности каламбура типизации посредством таких конструкций, как приведение типов, В языке Pascal записи с вариантами могут использоваться для интерпретации конкретного типа данных более чем одним способом, или даже не предусмотренным языком способом. Каламбур типизации является прямым нарушением типобезопасности. Традиционно возможность построить каламбур типизации связывается со слабой типизацией, но и некоторые сильно типизированные языки или их реализации предоставляют такие возможности (как правило, используя в связанных с ними идентификаторах слова ПримерыСтроки и числа в JavaScriptJS позволяет неявное приведение типов между строками и числами, что может приводить к нелогичным результатам, например: console.log(2 + 2) // 4
console.log("2" + "2") // "22"
console.log(2 + 2 - 2) // 2
console.log("2" + "2" - "2") // "20"
Оператор Сравнение в JavaScriptСравнение между значениями разных типов в JS не транзитивно: 0 == "0"
0 == []
"0" != []
Сокеты в СиКлассический пример каламбура типизации можно видеть в интерфейсе сокетов Беркли. Функция, которая связывает открытый неинициализированный сокет с IP-адресом, имеет такую сигнатуру: int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
Функция struct sockaddr_in sa = {0};
int sockfd = ...;
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
bind(sockfd, (struct sockaddr *)&sa, sizeof sa);
Библиотека сокетов Беркли в своей основе опирается на тот факт, что в языке Си указатель на В программировании часто встречается использование структур-«прослоек», позволяющих эффективно хранить различные типы данных в едином блоке памяти. Чаще всего такой трюк используется для взаимно исключающих данных с целью оптимизации. Числа с плавающей запятойПредположим, требуется проверить, что число с плавающей запятой является отрицательным. Можно было бы написать: bool is_negative(float x) {
return x < 0.0;
}
Однако, сравнения над числами с плавающей запятой являются ресурсоёмкими, так как действуют особым образом для NaN. Приняв во внимание, что тип bool is_negative(float x) {
return *((int*)&x) < 0;
}
Такая форма каламбура типизации является наиболее опасной. Предыдущий пример опирался только на гарантии, данные языком Си в отношении представления структур и преобразуемости указателей; однако, данный пример опирается на предположения в отношении конкретного аппаратного обеспечения. В некоторых случаях, например, при разработке приложений реального времени, которые компилятор не способен оптимизировать самостоятельно, такие опасные программные решения оказываются необходимыми. В таких случаях обеспечить поддерживаемость кода помогают комментарии и проверки времени компиляции (англ. Static_assertions). Реальный пример можно найти в коде Quake III — см. Быстрый обратный квадратный корень. В дополнение к предположениям о битовом представлении чисел с плавающей запятой вышеприведённый пример каламбура типизации также нарушает установленные языком Си правила доступа к объектам[3]: Использование unionПроблема совмещения имен[англ.] может быть решена посредством использования 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[4]. В качестве ещё одного примера каламбура типизации см. Stride of an array[англ.] (англ.). ПаскальВариантная запись позволяет рассматривать тип данных различным образом в зависимости от указанного варианта. В следующем примере предполагается, что 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));
Стандартная процедура PP^.L := 0;
PP := PP^.P; (* PP указывает на адрес 0 *)
K := PP^.L; (* K содержит значение слова по адресу 0 *)
Writeln(' Слово по адресу 0 данной машины содержит ', K);
Эта программа может работать корректно или обрушиться, если адрес 0 защищён от чтения, в зависимости от операционной системы. См. такжеПримечания
Ссылки
|
Portal di Ensiklopedia Dunia