В C++26 чтение неинициализированных переменных больше не является неопределенным, теперь оно «ошибочное» (Что такое ошибочное поведение? Чем оно отличается от неопределенного поведения?).
Однако меня смущает формулировка:
в противном случае байты имеют ошибочные значения, где каждое значение определяется реализацией независимо от состояния программы.
(bold mine)
На мой взгляд, это выглядит так, будто реализация должна чем-то перезаписать значения (например, 0xBEBEBEBE
), потому что если оставить их по-настоящему неинициализированными, это может сделать их зависимыми от «состояния программы», что противоречит выделенному жирным шрифтом фрагменту.
Верна ли моя интерпретация? Вынуждены ли реализации теперь перезаписывать неинициализированные переменные?
Согласно предложению C++26, реализации также будет разрешено диагностировать ошибочное поведение — либо во время трансляции, либо во время выполнения программы. Большинство современных компиляторов уже могут выдавать предупреждения об использовании неинициализированных переменных (хотя по историческим причинам эти предупреждения обычно отключены по умолчанию), поэтому, если это будет одобрено в C++26, я ожидаю, что большинство компиляторов просто выдадут диагностику, если осуществляется доступ к значению неинициализированной переменной.
Связанный P2795R5 говорит в разделе «Последствия для производительности и безопасности»:
- Автоматическое хранилище для автоматической переменной всегда полностью инициализируется, что потенциально может повлиять на производительность. P2723R1 подробно описывает затраты. Обратите внимание, что эта стоимость применяется даже тогда, когда создается переменная типа класса, не имеющая заполнения и чей конструктор по умолчанию инициализирует все члены.
- В частности, объединения полностью инициализируются. ...
Он также указывает, что, хотя автоматические локальные переменные могут быть аннотированы [[indeterminate]]
для подавления этой инициализации, нет способа избежать этого для каких-либо временных объектов.
Так что похоже ваша интерпретация верна.
Как ни странно, не кажется важным, что это за магическое значение — или даже то, действительно ли происходит эта инициализация — за исключением того, что это не может быть шаблон ловушки. Как уже указывалось, не существует магического значения байта, который был бы однозначно ошибочным во время выполнения и при этом безопасным для загрузки, копирования и сравнения.
Изменить - почему я говорю, что не имеет значения, каково магическое значение и даже происходит ли эта инициализация на самом деле?
Мотивация состоит в том, чтобы прекратить оценку (т. е. преобразование glvalue в prvalue) неинициализированных автоматических переменных, имеющих неопределенное поведение. Вместо этого это будет ошибочное поведение, которое рекомендуется диагностировать в реализациях.
Вышеупомянутое не может зависеть от конкретного битового шаблона, если этот битовый шаблон когда-либо может быть создан с помощью допустимого выражения без риска ошибочной диагностики.
Ни один обычный примитив не имеет таких волшебных битовых комбинаций, за исключением теперь редкого представления ловушки.
например. вы не можете использовать ни тихий, ни сигнальный NaN
для обозначения ошибочных значений, потому что если
double fine = std::numeric_limits<double>::quiet_NaN;
double errn;
std::isnan(fine); // not erroneous
std::isnan(errn); // erroneous behaviour
необходимо обрабатывать оба значения по-разному, оно не может основываться на битовом шаблоне.
То же самое тривиально верно и для целочисленных типов, и в любом случае [basic.indet/2] говорит
За исключением следующих случаев... если в результате оценки получено ошибочное значение, поведение является ошибочным, и результатом оценки является полученное таким образом значение, но не ошибочное.
где все исключения относятся к «беззнаковому обычному типу символов» и std::byte
, поэтому:
int errn; // erroneous value
foo(errn ^ 0); // 1, 2
foo(errn); // 3
foo
с неошибочным значением не должен диагностироватьсяfoo
с точно таким же битовым шаблоном может быть диагностированЕсли единственной целью является предотвращение утечки неинициализированных (автоматических) переменных в UB, достаточно требовать такого рода инициализации только для типов с представлениями-ловушками.
Также может потребоваться отключить (или защитить с помощью диагностических проверок) некоторые оптимизации, ранее разрешенные UB, но это не является ни необходимым, ни достаточным, чтобы это зависело от конкретного битового шаблона.
«или даже действительно ли эта инициализация происходит» Хм, вы хотите сказать, что формулировка выглядит так, будто она не требует принудительной инициализации? Потому что для меня «независимо от состояния программы» звучит так, будто вы не можете сохранить существующее значение.
Согласен, но, похоже, это не требует какого-либо поведения, которое на самом деле зависит от значения. Там определенно говорится, что это должно произойти, но далеко не ясно, почему.
Честно говоря, я не понимаю дискуссии о том, "важно... действительно ли происходит эта инициализация". Это либо требуется, либо не требуется, и кажется, что оно необходимо.
Требуется инициализация ошибочных значений, и подразумевается, что это связано с ошибочным поведением, но совершенно не понятно, почему. Требование дополнительной работы во время выполнения, которая ничего не дает, кажется странным, поэтому я надеялся, что кто-нибудь вмешается и предоставит обоснование.
Я могу предположить некоторые возможные оправдания. Это может предотвратить определенные уязвимости, связанные с предварительным заполнением неинициализированной переменной определенными значениями. Это также может помочь в отладке (даже если нет автоматического отчета, если вы видите 0xBEBEBEBE
(или что-то еще) в переменной, это сразу же говорит вам, что что-то не так). Он также устраняет случайное поведение при неинициализированном чтении, делая вызванные ими ошибки воспроизводимыми.
Это зависит от того, что вы подразумеваете под «некоторым фиксированным байтовым шаблоном».
Реализации могут выбирать значения, зависящие от типа переменной, поскольку это статическое свойство программы, не зависящее от ее состояния. В случаях, когда вы заявляете, например. выровненный буфер unsigned char
или std::byte
и последующее размещение в них новых объектов, обратите внимание, что это первый шаг, получение хранилища, которое записывает в память ошибочные значения; новый шаг размещения будет пустым, если он выполняет тривиальную инициализацию по умолчанию. Но компилятор может выполнить некоторый статический анализ, чтобы увидеть, какой тип памяти вы собираетесь предоставить для использования буфера, и соответствующим образом выбрать ошибочные значения.
Также теоретически возможно, что реализация может выбрать начальные ошибочные значения, используя настоящий генератор случайных чисел, но очевидно, что производительность этого будет ужасной, поэтому маловероятно, чтобы реальная реализация сделала это.
Обратите внимание, что начальные ошибочные значения не применяются к объектам со статической или потоковой продолжительностью хранения (которые инициализируются нулем) или динамической продолжительностью хранения. Также можно отказаться, используя атрибут [[indeterminate]]
.
В наши дни, когда анализ потоков данных в компиляторах настолько хорош, я ожидаю, что локальное использование неинициализированных переменных будет ошибкой времени компиляции, а не ошибки времени выполнения. Многие компиляторы в режиме отладки используют особый шаблон заполнения для неинициализированных переменных, но я ожидаю, что оптимизирующие компиляторы, по крайней мере, имеют возможность не беспокоиться об этом. Я также не уверен, как эта спецификация может быть реализована на практике, поскольку будет по крайней мере одно законное состояние программы, в котором переменная была правильно инициализирована магическим «неинициализированным» значением заполнения.