безопасность и корректностьsafety and correctness

Erroneous behavior для неинициализированных чтенийErroneous Behavior for Uninitialized Reads

Чтение неинициализированной скалярной переменной с автоматическим временем жизни больше не полное неопределённое поведение (UB), а новая категория — "erroneous behavior" (EB): результат чтения теперь определён (конкретное, но неспецифицированное значение), а реализациям предписано это диагностировать как баг.Reading an uninitialized scalar variable with automatic storage duration is no longer full undefined behavior (UB) — it's a new category, "erroneous behavior" (EB): the value read is now defined (some specific, unspecified value), and implementations are directed to diagnose it as a bug.

сужает, а не устраняет, класс UB — важная деталь области примененияnarrows, doesn't eliminate, a class of UB — an important scoping detail частый вопрос: чем EB отличается от UB и от defined-but-unspecifiedcommon question: how EB differs from UB and from defined-but-unspecified

Что этоWhat it is

P2795 меняет, что происходит при чтении неинициализированной скалярной переменной с автоматическим временем жизни. Раньше это было неопределённое поведение (UB — может произойти что угодно). Теперь это "erroneous behavior" (EB): прочитанное значение определено как некое конкретное, неспецифицированное, но согласованное значение (ожидается, что реализации будут использовать легко узнаваемый "ловушечный" битовый паттерн), и реализациям предписано (или требуется) диагностировать это как баг — во время выполнения, через санитайзеры/отладочную инструментацию, а в некоторых режимах сборки даже по умолчанию — при этом это НЕ лицензия компилятору считать путь кода недостижимым и оптимизировать исходя из "этого не может произойти", как позволяет настоящий UB.P2795 changes what happens when you read an uninitialized automatic-storage-duration scalar variable. Previously this was undefined behavior (UB — anything could happen). Now it's "erroneous behavior" (EB): the value read is defined to be some specific, unspecified-but-consistent value (implementations are expected to use an easily recognizable "trap"-like bit pattern), and implementations are directed to (or required to) diagnose it as a bug — at runtime, via sanitizers/debug instrumentation, or even by default in some build modes — while this is NOT a license for the compiler to assume the code path is unreachable and optimize based on "this never happens", the way true UB permits.

до C++26 — полное UB, компилятор может удалить проверкуbefore C++26 — full UB, the compiler may delete the check
bool is_ready;   // забыли инициализировать// forgot to initialize
// ... код, который должен был установить is_ready, но не сработал ...// ... code that was supposed to set is_ready, but didn't run ...

if (!is_ready) {
    abort_startup();   // теоретически компилятор мог счесть чтение UB// theoretically the compiler could treat the read as UB
}                         // и удалить всю проверку целиком// and remove the whole check entirely
C++26 — erroneous behavior, проверка обязана выполнитьсяC++26 — erroneous behavior, the check must still run
bool is_ready;   // чтение ниже — erroneous behavior, не UB// the read below is erroneous behavior, not UB

if (!is_ready) {
    abort_startup();   // значение конкретное (пусть и неверное) — ветка реально проверяется// value is concrete (if wrong) — the branch is genuinely evaluated
}                         // плюс санитайзер вправе тут же сообщить об ошибке// plus a sanitizer is entitled to flag this as a bug right here

Зачем ввелиWhy it was introduced

01

Классический UB давал компилятору право удалять проверки безопасностиClassical UB let the compiler delete safety checks

Под правилами UB компилятор юридически имел право предполагать, что чтение неинициализированной переменной не происходит — исторически это приводило к реальным, повторяющимся security-багам: проверка, написанная специально на случай "если это неинициализированное значение плохое", молча удалялась оптимизатором, потому что с точки зрения компилятора путь после UB-чтения был недостижим.Under UB rules, the compiler was legally permitted to assume a read of an uninitialized variable never happens — historically this led to real, recurring security bugs: a check written specifically for the "if this garbage-looking value is bad" case was silently removed by the optimizer, because from the compiler's perspective the path after a UB read was unreachable.

02

Одна из самых частых и опасных категорий багов C++One of the most common and dangerous classes of C++ bugs

Чтение неинициализированной переменной — массовый источник реальных ошибок, а не редкая экзотика. Сделать это "erroneous" вместо полностью неопределённого закрывает конкретную дверь: чтение теперь определено (приходит конкретное значение), но само по себе не даёт оптимизатору лицензии удалять окружающую логику, исходя из предположения "этого не может случиться".Reading an uninitialized variable is a mass source of real bugs, not a rare exotic case. Making this "erroneous" instead of fully undefined closes a specific door: the read is now defined (a concrete value comes back), but by itself gives the optimizer no license to delete surrounding logic based on the assumption "this can't happen".

03

Средний путь между "полностью неопределено" и "невидимо как баг"A middle ground between "fully undefined" and "invisible as a bug"

Полностью определить поведение (без флага "это баг") сделало бы ошибку невидимой навсегда. Оставить полным UB даёт оптимизатору слишком много прав. Erroneous behavior — компромисс: значение конкретно и код не взрывается непредсказуемо, но инструментарий обязан ловить это как ошибку.Making the behavior fully defined (with no "this is a bug" flag) would make the error invisible forever. Leaving it as full UB gives the optimizer too much license. Erroneous behavior is the compromise: the value is concrete and the code doesn't explode unpredictably, but tooling is directed to catch it as a bug.

Как это работает под капотомHow it works under the hood

Простое чтение до присваивания: int x; use(x); — до C++26 это UB (буквально что угодно допустимо); после — x читается как некое конкретное erroneous-but-defined значение, и от санитайзера/рантайм-проверки ожидается умение пометить это чтение как баг даже в оптимизированной сборке, в отличие от сегодняшнего дня, где для этого нужна специально инструментированная сборка (MemorySanitizer). Важная оговорка по области действия: это НЕ распространяется на память, полученную через new/malloc, и не касается игр с объединениями (unions) или reinterpret_cast — область действия — именно скалярные переменные с автоматическим временем жизни, и это честно ограниченная область, а не решение всех проблем неинициализированной памяти сразу.A simple read before assignment: int x; use(x); — before C++26 this is UB (literally anything is permitted); after, x reads as some specific erroneous-but-defined value, and a sanitizer/runtime check is expected to be able to flag this read as a bug even in an optimized build, unlike today where this requires a specially instrumented build (MemorySanitizer). An important scoping note: this deliberately does NOT apply to memory obtained via new/malloc, and doesn't touch union or reinterpret_cast type games — the scope is specifically automatic-storage-duration scalars, and this is honestly a limited scope, not a fix for every uninitialized-memory problem at once.

void use(int v);

void demo() {
    int x;          // не инициализирован// not initialized
    use(x);       // erroneous behavior: конкретное, но неверное значение, а не UB// erroneous behavior: a concrete but wrong value, not UB

    int* heap_x = new int;
    use(*heap_x);  // вне области действия EB — это по-прежнему классическое UB// outside EB's scope — this is still classical UB
}

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Классический security-релевантный случай: флаг, который забыли установитьThe classic security-relevant case: a flag that was never set

Условная ветка проверяет флаг, который должен был быть установлен раньше, но не был из-за бага. Под строгим UB проверка теоретически могла быть полностью удалена оптимизатором; под EB проверка обязана содержательно выполниться над реальным (пусть и неверным) значением.A conditional branch checks a flag that was supposed to be set earlier but wasn't, due to a bug. Under strict UB, the check could theoretically be optimized away entirely; under EB, the check must meaningfully execute against a real (if wrong) value.

bool permission_granted;   // путь установки был пропущен из-за бага// the setting path was skipped due to a bug

if (permission_granted) {   // EB: чтение конкретного значения, ветка реально проверяется// EB: a concrete value is read, the branch is really evaluated
    grant_access();
}
02

Неинициализированный счётчик цикла или аккумуляторAn uninitialized loop counter or accumulator

Цикл всё равно выполняется определённое (пусть и неверное) число раз, а аккумулятор накапливается от определённого (пусть и неверного) стартового значения, а не ведёт себя бессмысленно, как под полным UB.The loop still runs a defined (if wrong) number of times, and the accumulator accumulates from a defined (if wrong) starting value, rather than behaving nonsensically as under full UB.

int total;   // забыли total = 0;// forgot total = 0;
for (int v : values) {
    total += v;   // стартовое значение erroneous, но конкретное — не хаос// starting value is erroneous but concrete — not chaos
}
03

Обнаружение в CI без тяжёлой инструментации MemorySanitizerCI detection without MemorySanitizer's heavy instrumentation

Поскольку значение теперь достаточно специфицировано, более лёгкие стратегии обнаружения могут ловить erroneous-чтения автоматически в debug/CI-сборке — без отдельной, тяжёлой инструментированной сборки, которую сегодня требует MemorySanitizer.Because the value is now specified enough, lighter-weight detection strategies can catch erroneous reads automatically in a debug/CI build — without the separate, heavy instrumented build MemorySanitizer requires today.

// в debug-режиме сборки: чтение x ниже ожидаемо диагностируется// in a debug build mode: the read of x below is expected to be diagnosed
// как ошибка автоматически, без отдельного MSan-прохода// as an error automatically, with no separate MSan pass
int x;
use(x);
04

Самоинициализация как классический паттерн случайного багаSelf-initialization as a classic accidental bug pattern

int x = x; — по-прежнему баг и по-прежнему флагуется, но само ЧТЕНИЕ теперь определено правилами erroneous behavior, а не полным UB, — область поражения этого очень распространённого случайного паттерна сокращается.int x = x; — still a bug, still flagged, but the READ itself is now erroneous-behavior-defined instead of full UB — the blast radius of this very common accidental pattern shrinks.

int compute() {
    int x = x;   // опечатка: имелось в виду другое имя справа// a typo: a different name was meant on the right
    return x;    // EB, не полное UB — уже прогресс, но всё ещё баг// EB, not full UB — progress, but still a bug
}

ГраблиGotchas

Область действия — только скалярные автоматические переменныеScope is limited to scalar automatic variables

Неинициализированная память в куче (new/malloc без инициализации), неинициализированные члены агрегатов в некоторых контекстах и другие категории "чтения памяти, которую не следовало читать" остаются вне действия этой фичи и по-прежнему могут быть классическим UB. Это частичное, а не полное решение проблемы неинициализированной памяти.Uninitialized heap memory (via new/malloc without initialization), uninitialized aggregate members in some contexts, and other categories of "reading memory you shouldn't" remain outside this feature's defined behavior and can still be classical UB. This is a partial, not total, fix for the broader uninitialized-memory problem.

Определённость не означает, что баг стал нормальнымBeing defined doesn't mean the bug is now acceptable

То, что erroneous behavior определено, не означает, что баг исчезает или становится приемлемым — это по-прежнему определённо баг в программе. Изменение касается того, что ЯЗЫКУ/КОМПИЛЯТОРУ разрешено делать в ответ, а не того, что чтение стало давать осмысленное или полезное значение."Erroneous behavior" being defined does NOT mean the bug goes away or becomes acceptable — it's still definitely a bug in the program. The change is about what the LANGUAGE/COMPILER is permitted to do in response, not about the read producing a meaningful or useful value.

Определённость — не повод полагаться на код с этим чтениемBeing defined is not a license to rely on such code

Рассуждать "раз это теперь не 'что угодно может случиться', значит мой код, случайно читающий неинициализированные данные, в порядке" — ровно наоборот тому, что задумано. Смысл EB — сделать такие баги НАДЁЖНЕЕ отлавливаемыми инструментами, а не легитимизировать сам баг.Reasoning "since it's no longer 'anything can happen', my code that accidentally reads uninitialized data is fine because it's defined now" is exactly backwards. The intent of EB is to make such bugs MORE reliably caught by tooling, not to legitimize the bug.