Loading AI tools
термін в інформатиці для позначення різних технік порушення або «обману» системи типів мови програмування З Вікіпедії, вільної енциклопедії
Каламбур типізації (англ. type punning) — термін в інформатиці для позначення різних технік порушення або «обману» системи типів деякої мови програмування, які мають ефект, який було б складно або неможливо забезпечити в рамках формальної мови.
Мови C і C++ надають явні можливості каламбуру типізації за допомогою таких конструкцій, як зведення типів, union
, а також reinterpret_cast
для C++, хоча стандарти цих мов деякі випадки таких каламбурів трактують як невизначену поведінку.
У мові Pascal записи з варіантами дозволяють інтерпретувати конкретний тип даних більш, ніж в один спосіб, або навіть у спосіб, не передбачений мовою.
Каламбур типізації є прямим порушенням типобезпеки. Традиційно можливість побудувати каламбур типізації пов'язують зі слабкою типізацією, але й деякі сильно типізовані мови або їх реалізації надають такі можливості (як правило, використовуючи у пов'язаних з ними ідентифікаторах слова unsafe
або unchecked
). Прихильники типобезпеки стверджують, що «необхідність» каламбурів типізації є міфом[1].
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);
Бібліотека сокетів Берклі у своїй основі спирається на той факт, що в мові C вказівник на struct sockaddr_in
може безперешкодно перетворюватися на вказівник на struct sockaddr
, а також що обидва структурні типи частково збігаються щодо організації подання в пам'яті. Отже, вказівник на поле my_addr->sin_family
(де my_addr
має тип struct sockaddr*
) насправді вказуватиме на поле sa.sin_family
(де sa
має тип struct sockaddr_in
). Іншими словами, бібліотека використовує каламбур типізації для реалізації примітивної форми наслідування[2].
У програмуванні часто зустрічається використання структур-"прошарків", що дозволяють ефективно зберігати різні типи даних у єдиному блоці пам'яті. Найчастіше такий трюк використовують для взаємно виключних даних із метою оптимізації.
Припустимо, потрібно перевірити, що число з рухомою комою є від'ємним. Можна було б написати:
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;
}
Така форма каламбуру типізації є найнебезпечнішою. Попередній приклад спирався лише на гарантії, надані мовою C щодо подання структур та перетворюваності вказівників; однак, цей приклад спирається на припущення щодо конкретного апаратного забезпечення. У деяких випадках, наприклад, під час розробки програм реального часу, яких компілятор не здатний оптимізувати самостійно, такі небезпечні програмні рішення виявляються необхідними. У таких випадках забезпечити підтримуваність коду допомагають коментарі та перевірки часу компіляції.
Реальний приклад можна знайти в коді Quake III — див. Швидкий обернений квадратний корінь.
На додаток до припущень про бітове подання чисел з рухомою комою наведений вище приклад каламбуру типізації також порушує встановлені мовою C правила доступу до об'єктів[3]: 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[4].
Як ще один приклад каламбуру типізації див. Крок масиву[en].
Варіантний запис дозволяє розглядати тип даних по-різному, залежно від зазначеного варіанту. У цьому прикладі передбачається, що 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 захищено від читання, залежно від операційної системи.
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.