EnglishРусский Map
C3

Беззнаковые размеры - пятилетняя ошибка в C3

title
Беззнаковые размеры - пятилетняя ошибка в C3
type
summary
summary
Создатель C3 о том, почему делать размеры по умолчанию беззнаковыми было ошибкой, и почему Go и Java поступили верно
parent
c3-lang
tags
language-design, type-systems, c3
created
2026-05-03
updated
2026-05-03
lang
ru
source_updated
2026-05-03
translated
2026-09-01
translator
lllm/antigravity/gemini-3.7-flash-medium

Майский пост Кристоффера Лерно (Christoffer Lerno) за 2026 год, где объясняется, почему C3 спустя пять лет меняет тип размера по умолчанию с беззнакового на знаковый. Подача необычна: большинство заметок в духе "мы изменили настройки по умолчанию" посвящены эргономике; здесь же доказывается, что исходное решение было внутренне противоречивым на уровне дизайна языка, и компилятор не способен это замаскировать.

Цепочка, берущая начало от sizeof

Ключевой тезис: все хрестоматийные баги с беззнаковыми числами в C происходят от одного раннего решения - стандартизации sizeof как size_t. Это сделало беззнаковую арифметику обычным делом в коде на C, нормализовало идиому типизации "раз не может быть отрицательным, значит беззнаковое" и перекочевало во все языки-наследники, перенявшие соглашение C о размерах, включая Rust и Zig.

Как только размеры становятся беззнаковыми, индексация тоже становится беззнаковой. А когда индексация беззнаковая, смешивание знаковых и беззнаковых типов оказывается повсюду, и языку приходится либо разрешать неявное приведение (тихие баги), либо требовать явных cast'ов на каждом шагу (эргономическая катастрофа, cast'ы превращаются в "заткнуть все предупреждения").

Баги, которые разбирает Лерно

  • for (uint x = 10; x >= 0; x--) - бесконечный цикл. В C3 пришлось делать специальное исключение для такого сравнения.
  • uint a = 0; int b = -1; if (a > b) - продвижение к беззнаковому типу (promotion), знак сравнения инвертируется. В C3 добавили безопасные смешанные сравнения без приведения.
  • Засилье cast'ов в стиле Rust - в коде накапливаются преобразования, которые "компилируются, но врут", если тип операнда под cast'ом впоследствии изменится.

В C3 выбрали путь минимизации cast'ов: int + uint продвигался к int, чтобы большинство выражений незаметно оставались знаковыми. Это работало пять лет.

Где всё сломалось: деление и остаток

Поворотной точкой в статье служит один пример: (foo + a) % 2. По правилам продвижения типов в C3 это даёт заведомо неверный результат, когда foo > INT_MAX (правильный вариант - % 2U). В остальных местах можно было не задумываться, является ли подвыражение знаковым: / и % были исключением, а удобство, скрывавшее неявное приведение, лишь мешало это исключение заметить.

Быстрым решением было выдавать ошибку на unsigned / signed и unsigned % signed. Но куда более глубокая проблема вскрылась на примере кольцевого буфера.

Кольцевой буфер

Зацикливание при отрицательном смещении в кольцевом буфере:

index = ((start + offset) % length + length) % length;  // signed: works
index = ((start - offset_back) % length + length) % length;  // unsigned: silently wrong
index = (start + length - (offset_back % length)) % length;  // unsigned: correct

Сломанный беззнаковый вариант зачастую случайно заворачивается правильно, поэтому проходит поверхностное тестирование. Никакой компилятор не укажет на ошибку: с точки зрения синтаксиса и системы типов здесь всё чисто.

Это единственный пример в статье, который Лерно считает принципиально нерешаемым, а не просто неэргономичным. С беззнаковыми размерами естественный код неверен, а корректный код уродлив. Дизайн языка навязывает компромисс, которого со знаковыми размерами просто не существует.

Аргумент про диапазон значений не выдерживает критики

Стандартный довод в защиту беззнаковых типов - "удвоение диапазона". Контраргумент Лерно: переполнение знакового числа даёт заведомо некорректное отрицательное значение; переполнение беззнакового даёт правдоподобно выглядящее положительное число - тихо ошибочное и трудноуловимое. К тому же на 64-битных машинах память закончится задолго до достижения 2^63.

Он также отмечает, что превращение беззнакового переполнения в ошибку (путь Rust) ломает алгебраическое тождество (a + b) - c == a + (b - c), справедливое только при разрешённом циклическом переполнении (wrapping). Так что ужесточение правил для беззнаковых типов лишь меняет одну ловушку на другую.

Опыт работы с фреймворками верификации (упомянутый вскользь) показывает, что беззнаковый тип кодирует смысл "значение циклически переполняется по модулю 2^N", а не "значение лежит в некотором диапазоне". Так что аргумент "тип как предусловие" тоже не выдерживает проверки инструментами доказательства корректности.

Как поступили другие языки

  • Java полностью отказалась от беззнаковых типов ещё в 90-х. Лерно называет это решение "пожалуй, чересчур радикальным", но признаёт, что оно уничтожило целый класс багов.
  • В Go, созданном людьми, прекрасно знавшими цену беззнаковых размеров в C++, выбрали знаковые размеры. Тот факт, что это "низкоуровневый системный язык от людей с реальным опытом", делает его сильнейшим контраргументом.
  • C++, Rust и Zig - все они продолжают традицию беззнаковых размеров из C.

Что изменили в C3

  • isz / usz переименованы в sz / usz: асимметричная пара явно указывает, какой тип предпочтителен.
  • Неявное преобразование signed <-> unsigned: убрано.
  • Смешанные сравнения знаковый-беззнаковый: убраны.
  • В сообществе C3 это изменение получило название "szmageddon" (изначально "iszmageddon").

Более глубокий вывод

Размышления Лерно в конце статьи заслуживают внимания независимо от деталей самого языка: он свыкся с издержками беззнаковых типов. Проведя достаточно лет с C/C++, перестаёшь замечать когнитивную нагрузку от вопроса "безопасно ли это выражение с точки зрения беззнаковых типов?" - просто платишь эту цену. Поначалу изменение казалось неправильным, "будто я совершаю нечто запретное". Сигналом ошибочности исходного решения стал не баг, а осознание того, что привычка маскировала реальную цену.

Это аргумент о лени как добродетели, применённый к дизайну языков: дисциплина, которую инженеры вырабатывают для компенсации недостатков инструмента, становится невидимой, а устранение дефекта воспринимается как разрушение дисциплины.

См. также

  • c3-lang - язык и его блог
  • signed-vs-unsigned-sizes - концепция более общего уровня, выделенная из этой статьи
  • zig - выбрал беззнаковые размеры; в статье он напрямую отнесён к C/C++/Rust как совершающий ту же ошибку
  • type-systems-vocabulary - другой аспект проблемы: "создатели языков не сходятся во мнении, зачем нужны типы"
  • message-passing-shared-mutable-state - публикация похожей структуры: "канонический ответ в давнем споре неверен"