Знаковые и беззнаковые размеры
- title
- Знаковые и беззнаковые размеры
- type
- concept
- summary
- Выбор между знаковыми и беззнаковыми типами для размеров: классы багов для каждого варианта и опыт разных языков программирования
- tags
- language-design, type-systems
- created
- 2026-05-03
- updated
- 2026-05-03
- lang
- ru
- translation_of
- signed-vs-unsigned-sizes
- source_updated
- 2026-05-03
- translated
- 2026-09-01
- translator
- lllm/antigravity/gemini-3.7-flash-medium
Выбор типа по умолчанию для размеров, длин и индексов: знаковый или беззнаковый. В C его сделали беззнаковым (size_t), и большинство наследников скопировали этот подход; в Java от беззнаковых типов отказались вовсе; в Go выбрали знаковый; C3 перешёл с беззнакового на знаковый в 2026 году после пяти лет с беззнаковым по умолчанию.
Это решение распространяется по всей системе типов: беззнаковые размеры влекут за собой беззнаковую индексацию, а это означает беззнаковую арифметику в любом коде, работающем с длинами массивов. Отсюда постоянное смешивание знаковых и беззнаковых типов, вынуждающее язык полагаться либо на неявные приведения (тихие баги), либо на повсеместные явные приведения (культура спама cast'ами).
Классы багов, которые порождает беззнаковый тип по умолчанию
- Бесконечный цикл при обратном отсчёте.
for (uint x = 10; x >= 0; x--)- условие никогда не станет ложным. Паттерн настолько распространён, что в C3 сделали>= 0для беззнаковых типов отдельной ошибкой компиляции. - Инверсия сравнения знакового и беззнакового.
uint a = 0; int b = -1; if (a > b)- C повышает тип до беззнакового, поэтомуbпревращается в огромное положительное число, и результат сравнения неверен. Языкам с неявным приведением типов нужно явное правило "безопасного сравнения", чтобы это исправить. - Правдоподобное переполнение. Знаковое переполнение даёт явно некорректное отрицательное число; беззнаковое переполнение даёт правдоподобное положительное число, которое просто неверно. В беззнаковом варианте ошибка часто проскакивает поверхностное тестирование.
- Переход через границу в кольцевом буфере.
((start - offset_back) % length + length) % length- естественный паттерн с отрицательным смещением тихо ломается на беззнаковых значениях, и никакое правило системы типов не способно это отловить. Корректный беззнаковый вариант выглядит куда более громоздко и неестественно. - Асимметрия деления и остатка от деления.
(foo + a) % 2даёт один результат, когдаfooзнаковый, и совершенно другой, когда тип повышается до беззнакового. Даже с правилами безопасного приведения/и%обнажают знаковость так, как этого не делают+,-и*.
Классы багов, которые порождает знаковый тип по умолчанию
- Попытки использовать отрицательный индекс массива. Отлавливаются во время выполнения при проверке границ; без более выразительной системы типов язык не может поймать их статически.
- Ошибка на единицу около нуля. Возможна, но результатом обычно становится обычное отрицательное число, а не переполнение с уходом в миллиарды.
- Теоретическая потеря диапазона. Половина диапазона значений уходит под отрицательные числа, которые в основном не нужны. На 64 битах память закончится задолго до достижения 2^63, так что потерянная половина роли не играет. На 32 битах адресация дальше 2 ГБ требуется настолько редко, что в языках вроде Go с этим просто смирились.
Кто что выбрал
| Язык | Размеры | Примечания |
|---|---|---|
| C | беззнаковые (size_t) |
Выбор, который разошёлся по остальным |
| C++ | беззнаковые | Унаследовано из C; за знаковую индексацию выступали Страуструп и Саттер |
| Rust | беззнаковые (usize) |
Переполнение вызывает ошибку во время выполнения, а не wrap |
| Zig | беззнаковые (usize) |
То же соглашение, что и в Rust |
| Java | только знаковые | Беззнаковых типов нет вообще |
| Go | знаковые (int) |
Выбор команды, знавшей проблемы C++ на собственном опыте |
| C3 | знаковые (sz) с версии 0.8.0 |
Перешли в 2026 году после 5 лет с беззнаковыми |
Дихотомия: спам cast'ами против минимизации приведений
Отдельный, но связанный выбор: когда встречаются знаковый и беззнаковый типы, язык выполняет неявное приведение или требует явного cast'а?
- Подход со спамом cast'ами. Приведения типов писать несложно, "они документируют преобразование". Минус: cast, корректный на момент написания, начинает молча отсекать биты при изменении вышележащего типа. Приведения превращаются в способ "заглушить все предупреждения".
- Подход с минимизацией cast'ов. Явные приведения означают опасную зону. В теории это чище, но для таких правил сложнее выработать спецификацию. C3 пошёл по этому пути, из-за чего проблема беззнаковых размеров встала в полный рост: беззнаковые размеры требуют постоянных преобразований между знаковыми и беззнаковыми типами, а язык, стремящийся минимизировать cast'ы, вынужден делать большинство таких преобразований неявными.
Показателен опыт Rust: беззнаковые размеры вкупе с принципом "явные приведения везде" приводят к кодовым базам, переполненным вызовами as usize и as i64. Вставленные машинально, они накапливаются и на практике не ловят баги, которые должны были документировать.
Аргумент "беззнаковый тип кодирует предусловие"
Логика выбора беззнаковых типов по умолчанию строится на том, что "это значение не может быть отрицательным" - полезное предусловие, и беззнаковый тип фиксирует его в системе типов. Этот довод не выдерживает проверки инструментами верификации: беззнаковые типы кодируют "значение заворачивается по модулю 2^N", а не "значение лежит в диапазоне [0, N)". Предусловие всегда оставалось неформальным соглашением, а "гарантия" со стороны системы типов оказывается вовсе не той гарантией.
Наблюдение о привыкании к скрытым издержкам
Самый любопытный нетехнический вывод из ретроспективы C3 заключается в том, что опытные программисты на C/C++ перестают замечать цену повсеместного использования беззнаковых типов: они несут эту когнитивную нагрузку на автомате и забывают о её существовании. Это аргумент о лени как добродетели, применённый наоборот: дисциплина, компенсирующая изъян инструмента, становится невидимой, и устранение изъяна воспринимается как отказ от дисциплины.
См. также
- unsigned-sizes-c3-mistake - канонический подробный пример этой концепции
- c3-lang - разбор на примере реального языка
- zig, zig-functional-programmers - в Zig сохранили беззнаковые размеры; полезный контраст
- type-systems-vocabulary - смежные споры о терминологии систем типов
- no-silver-bullet - компромиссы при проектировании языков в целом