безопасность и корректностьsafety and correctness
Чтение неинициализированной скалярной переменной с автоматическим временем жизни больше не полное неопределённое поведение (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.
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.
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
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
Под правилами 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.
Чтение неинициализированной переменной — массовый источник реальных ошибок, а не редкая экзотика. Сделать это "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".
Полностью определить поведение (без флага "это баг") сделало бы ошибку невидимой навсегда. Оставить полным 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.
Простое чтение до присваивания: 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
}
Условная ветка проверяет флаг, который должен был быть установлен раньше, но не был из-за бага. Под строгим 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();
}
Цикл всё равно выполняется определённое (пусть и неверное) число раз, а аккумулятор накапливается от определённого (пусть и неверного) стартового значения, а не ведёт себя бессмысленно, как под полным 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
}
Поскольку значение теперь достаточно специфицировано, более лёгкие стратегии обнаружения могут ловить 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);
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
}
Неинициализированная память в куче (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.
То, что 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.
Рассуждать "раз это теперь не 'что угодно может случиться', значит мой код, случайно читающий неинициализированные данные, в порядке" — ровно наоборот тому, что задумано. Смысл 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.