Каламбур типізації — Вікіпедія
Каламбур типізації (англ. type punning) — термін в інформатиці для позначення різних технік порушення або «обману» системи типів деякої мови програмування, які мають ефект, який було б складно або неможливо забезпечити в рамках формальної мови.
Мови C і C++ надають явні можливості каламбуру типізації за допомогою таких конструкцій, як зведення типів, union
, а також reinterpret_cast
для C++, хоча стандарти цих мов деякі випадки таких каламбурів трактують як невизначену поведінку.
У мові Pascal записи з варіантами дозволяють інтерпретувати конкретний тип даних більш, ніж в один спосіб, або навіть у спосіб, не передбачений мовою.
Каламбур типізації є прямим порушенням типобезпеки. Традиційно можливість побудувати каламбур типізації пов'язують зі слабкою типізацією, але й деякі сильно типізовані мови або їх реалізації надають такі можливості (як правило, використовуючи у пов'язаних з ними ідентифікаторах слова unsafe
або unchecked
). Прихильники типобезпеки стверджують, що «необхідність» каламбурів типізації є міфом[1].
Приклади[ред. | ред. код]
Рядки та числа в JavaScript[ред. | ред. код]
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
.
Порівняння в JavaScript[ред. | ред. код]
Порівняння між значеннями різних типів JS не транзитивне:
0 == "0" 0 == [] "0" != []
Сокети в C[ред. | ред. код]
Класичний приклад каламбуру типізації можна побачити в інтерфейсі сокетів Берклі. Функція, яка пов'язує відкритий неініціалізований сокет з 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[ред. | ред. код]
Проблему суміщення назв можна вирішити за допомогою 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 захищено від читання, залежно від операційної системи.
Див. також[ред. | ред. код]
Примітки[ред. | ред. код]
- ↑ Lawrence C. Paulson. ML for the Working Programmer. — 2nd. — Cambridge, Great Britain : Cambridge University Press, 1996. — С. 2. — ISBN 0-521-57050-6 (тверда обкладинка), 0-521-56543-X (м'яка обкладинка).
- ↑ struct sockaddr_in, struct in_addr. www.gta.ufrj.br. Архів оригіналу за 24 січня 2016. Процитовано 17 січня 2016.
- ↑ ISO/IEC 9899:1999 s6.5/7
- ↑ GCC: Non-Bugs. Архів оригіналу за 22 листопада 2014. Процитовано 21 листопада 2014.
Посилання[ред. | ред. код]
- Теорія
- Benjamin C. Pierce. Types and Programming Languages. — MIT Press, 2002. — ISBN 978-0-262-16209-8.
- Розділ посібника з компілятора GCC щодо опції -fstrict-aliasing, що запобігає деяким каламбурам типізації
- Defect Report 257, що випадково визначає «каламбур типізації» за допомогою
union
і обговорює поведінку наведеного вище коду, що залежить від реалізації . - Defect Report 283 про використання типу
union
для каламбурів типізації
- Типобезпечні мови
- Unchecked Conversion (мова Ada)
- The Unsafe structure (мова Standard ML)
- Haskell unsafeCoerce (мова Haskell)
|