Урок по треку C++ 6 мин

Типы и неопределённое поведение

Целочисленные типы и продвижения, переполнение, UB, implementation-defined и unspecified, strict aliasing.

C++ даёт программисту много свободы, а компилятору ещё больше. Почти на каждом собеседовании по C++ проверяют, понимаете ли вы, где заканчиваются гарантии языка. Начнём с типов.

Целочисленные типы

Стандарт гарантирует не размеры, а минимальные диапазоны: char ровно 1 байт, short и int не меньше 16 бит, long не меньше 32, long long не меньше 64. На практике:

Модельintlongуказатель
Linux, macOS x86-64 (LP64)326464
Windows x64 (LLP64)323264

Отсюда правило: если важен размер, берите std::int32_t, std::int64_t, std::uint8_t из <cstdint>, а для размеров и индексов std::size_t. Знаковость char зависит от платформы: на x86 он обычно знаковый, на ARM Linux беззнаковый.

С C++20 знаковые целые обязаны храниться в дополнительном коде (two's complement). Но переполнение знакового остаётся неопределённым поведением. Беззнаковые же считаются по модулю 2ⁿ: 0u - 1 равно UINT_MAX, это определено.

Продвижения и преобразования

Перед арифметикой типы меньше int (char, short, bool) продвигаются до int. Затем операнды приводятся к общему типу по «обычным арифметическим преобразованиям». Главная ловушка: при смешении int и unsigned int знаковый операнд превращается в беззнаковый.

std::cout << (-1 < 1u);        // 0: -1 стал 4294967295
std::vector<int> v;
for (int i = 0; i < v.size() - 1; ++i) {}  // v.size() - 1 == SIZE_MAX при пустом векторе

Деление целых обрезает к нулю: -7 / 2 == -3, а -7 % 2 == -1 (знак остатка как у делимого, гарантировано с C++11).

Три вида «как повезёт»

  • Неопределённое поведение (UB): стандарт ничего не требует. Программа может работать, упасть, выдать мусор или вести себя по-разному при -O0 и -O2.
  • Implementation-defined: поведение выбирает реализация и обязана его документировать: sizeof(int), знаковость char.
  • Unspecified: допустимо несколько вариантов, документировать не надо: порядок вычисления аргументов функции f(a(), b()).

Типичные источники UB:

  • переполнение знакового: INT_MAX + 1;
  • выход за границы массива, разыменование nullptr или висячего указателя;
  • чтение неинициализированной локальной переменной;
  • сдвиг на число бит ≥ ширины типа или на отрицательное: 1 << 32 для 32-битного int;
  • нарушение strict aliasing: чтение float через int*;
  • гонка данных: два потока без синхронизации, хотя бы один пишет;
  • выход из не-void функции без return (кроме main).

Почему компилятор «ломает» код с UB

Оптимизатор вправе считать, что UB никогда не происходит, и строить выводы на этом.

bool will_overflow(int x) { return x + 1 < x; }   // часто компилируется в return false
int get(int* p) { int v = *p; if (!p) return 0; return v; } // проверку p убирают: p уже разыменован

В первом случае переполнение знакового UB, значит x + 1 < x «никогда» не истинно. Во втором после *p компилятор знает, что p не нулевой. Поэтому «у меня работает» ничего не доказывает.

Как жить

  • Компилируйте с -Wall -Wextra -Wconversion и прогоняйте тесты с -fsanitize=address,undefined.
  • Проверяйте переполнение до операции или через __builtin_add_overflow и родственные встроенные функции GCC и Clang, либо считайте в более широком типе.
  • Перетолковывайте байты через std::memcpy или std::bit_cast (C++20), а не через приведение указателей.
  • Не смешивайте знаковые и беззнаковые в сравнениях; std::ssize(v) (C++20) возвращает знаковый размер.

Частые ошибки

  • Думать, что знаковое переполнение «просто завернётся», раз C++20 закрепил дополнительный код.
  • Писать for (size_t i = n - 1; i >= 0; --i): условие всегда истинно, беззнаковое не бывает меньше нуля.
  • Считать UB «чем-то на редких платформах»: современные GCC и Clang активно используют его при оптимизации.

Что спросят на собесе

  • Чем UB отличается от implementation-defined и unspecified, примеры каждого.
  • Почему -1 < 1u ложно и как это ловить.
  • Что изменилось в C++20 для знаковых целых и что осталось UB.
  • Как компилятор использует UB для оптимизаций и как искать UB (санитайзеры).
  • Как безопасно перетолковать байты одного типа как другой.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.