Беззнаковые размеры - пятилетняя ошибка в C3
- title
- Беззнаковые размеры - пятилетняя ошибка в C3
- type
- summary
- summary
- Создатель C3 о том, почему делать размеры по умолчанию беззнаковыми было ошибкой, и почему Go и Java поступили верно
- parent
- c3-lang
- tags
- language-design, type-systems, c3
- sources
- unsigned-sizes-c3-mistake
- created
- 2026-05-03
- updated
- 2026-05-03
- lang
- ru
- translation_of
- unsigned-sizes-c3-mistake
- 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 - публикация похожей структуры: "канонический ответ в давнем споре неверен"