Каламбур типізації

термін в інформатиці для позначення різних технік порушення або «обману» системи типів мови програмування

Каламбур типізації (англ. 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 захищено від читання, залежно від операційної системи.

Див. також ред.

Примітки ред.

  1. 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 (м'яка обкладинка).
  2. struct sockaddr_in, struct in_addr. www.gta.ufrj.br. Архів оригіналу за 24 січня 2016. Процитовано 17 січня 2016.
  3. ISO/IEC 9899:1999 s6.5/7
  4. GCC: Non-Bugs. Архів оригіналу за 22 листопада 2014. Процитовано 21 листопада 2014.

Посилання ред.

Теорія
Мова C — звіти про дефекти стандарту C99
  • Розділ посібника з компілятора GCC щодо опції -fstrict-aliasing, що запобігає деяким каламбурам типізації
  • Defect Report 257, що випадково визначає «каламбур типізації» за допомогою union і обговорює поведінку наведеного вище коду, що залежить від реалізації .
  • Defect Report 283 про використання типу union для каламбурів типізації
Типобезпечні мови