Comments 28
Допустим, программист читает неинициализированную переменную по ошибке. Тогда присвоение какого-то начального значения никак не сделает его код правильным.
Допустим, программист читает её умышленно (например, пишет программу-шпион). Тогда что ему делать?
Ну и наконец, что будет, если я объявлю массив на терабайт? Он тоже будет расписываться этой шнягой?
Или это всё действует только в отношении одиночных элементарных значений, но не массивов и не разыменованных указателей (ссылок)? Тогда какой вообще смысл?
Допустим, программист читает её умышленно (например, пишет программу-шпион). Тогда что ему делать?
В C++26 и позднее написать [[indeterminate]]
, а до того передавать ключ -ftrivial-auto-var-init=uninitialized
или вообще никакого не ставить.
Ну и наконец, что будет, если я объявлю массив на терабайт? Он тоже будет расписываться этой шнягой?
В GCC опция pattern делает цикл на весь массив: https://godbolt.org/z/YY41Mr13d, Clang делает то же самое через memset: https://godbolt.org/z/4odYqWfo5
Или это всё действует только в отношении одиночных элементарных значений, но не массивов и не разыменованных указателей (ссылок)? Тогда какой вообще смысл?
Не получилось почему-то нагуглить это в документации для GCC, нашёл только для Arm Compiler (по сути то же самое). Получается, для элементарных значений (в том числе и указателей!) память заполняется байтами 0xAA или 0xFF, массивы заполняются значениями согласно своим элементам, структуры — поэлементно, юнионы — согласно варианту с большей длиной. В грубом приближении можно считать, что в x86-64 все неицилизированные переменные будут заполнены 0xAA. И, что самое страшное, указатели тоже.
Думаю, что такое сделано, чтобы подобные участки памяти было видно в отладчике, но всё же это выглядит довольно странно, и если программа не подвергается даже минимальному статическому анализу хотя бы на уровне компиляторовского -Wall
, который покажет обращения к неициализированным переменным, то непонятно, кто тогда вообще будет её прогонять в отладчике.
Ну понятно, что в качестве специальной опции компилятора это осмысленное средство. Но как стандартная семантика языка – выглядит странно.
Да, тоже не очень понял. По логике, раз это erroneous behavior, компилятор должен давать ошибку компиляции. Если же вместо этого подобный код будет молча инициализироваться какой-то фигнёй, это выглядит ещё большим вредительством.
Как раз 0xAA отличное значение для заполнения байтов. На 64-битных машинах это заведомо невалидный указатель, любое обращение к нему приведёт к сегфолту. Это лучшее, что можно сделать. На 32 битах лучше 0xCC (0xAAAA_AAAA это в принципе валидный байтовый/вордовый адрес в режиме ядра 3Gb)
Если расценивать это как число любого типа, неважно, int, unsigned, float или double, это будет абсурдно большое число (во всех случаях кроме unsigned - отрицательное). Что бы вы там ни считали, скорее всего вы получите заведомо абсурдные результаты. Это намного лучше, чем, допустим, 0. Если вместо нуля ожидалось какое-то малое слагаемое, его отсутствие можно и не заметить.
А вообще, лично мой идеал это числа 0x7fff_xyz0, где x,y и z- любые 16-ричные цифры, для 32 бит, и то же самое, только ширше в середине, для 64 бит. Они соответствуют float NaN-ам, выполняют пункт про "абсудрно большие" целые и указывают на область стека функции main или компиляторной обёртки вокруг main. Собственно, специальные значения типа undefined
в javascript именно так обычно и делают.
Надо посмотреть во что оно скомпилится. Может, переменной вовсе не будет...
имхо, достаточно просто падать с ошибкой, при компиляции таких переменных. И пусть программист сам решает чем проиницилизировать. Не всегда дефолтное значение это синоним безопасного поведения.
Недавно я получил по почте от Сэма Джонсона этот вопрос.
Кто это? Мы должны его знать?
Довольно странное решение. Я бы понял решение в духе C# – когда на всех путях переменная до чтения гарантированно должна быть присвоена, иначе программа не скомпилируется, но присваисвание ей какого-то неизвестного значения (0xDEADBEEF, что ли?) не выглядит серьёзным улучшением по сравнению с мусором из стека.
Я бы понял решение в духе C# – когда на всех путях переменная до чтения гарантированно должна быть присвоена, иначе программа не скомпилируется
Это нереально. Простой пример:
// Модуль A
void foo(int&);
// Модуль B
int main()
{
int a;
foo(a);
++a;
}
Предположим что модуль A распространяется в виде уже скомпилированной библиотеки (DLL или .so). Откуда компилятор знает, что делает функция foo()
? Инициализирует ли она переменную a
? А может быть наоборот, она из нее только читает? Он не может быть в этом уверен. Если в подобных случаях заставлять погромиста явно инициализировать a
до вызова foo()
, то нужно будет править огромное количество существующего кода, который совершенно нормально работает, чисто для того, чтобы удовлетворить компилятор. Никто на это не пойдет.
но присваисвание ей какого-то неизвестного значения (0xDEADBEEF, что ли?) не выглядит серьёзным улучшением по сравнению с мусором из стека.
Дело не в "мусоре из стека", а в том, что сейчас компилятор считает, что чтение неинициализированной переменной просто не может происходить. И делает разнообразные оптимизации (например, выкидывание "лишнего" по его мнению кода) исходя из этого. После введения erroneous behavior компилятор уже будет в курсе, что такое чтение происходить может, просто это ненормально. И будет заниматься преобразованием и оптимизацией кода исходя уже из новых представлений.
Да я знаю про оптимизации. И понятно, что компилятор теряет возможность проводить оптимизации, полагаясь на то, что программист не будет читать переменную до записи в неё. Т.е. возможный выигрыш в быстродействии потеряли, а от ошибок не избавились.
Конкретно в приведённом листинге проблема только в том, что в синтаксисе C++ не указывается направление передачи параметров.
В каком-нибудь Фортране, если параметр описан как intent(in)
или intent(inout)
, то это некорректный код, а если как intent(out)
– то корректный. И компилятор, соответственно, это сечёт.
Лучше б в C++ добавили intent.
Лучше б в C++ добавили intent.
Даже если предположить, что кто-то решит сделать нечто подобное, то, во-первых, это ничего не гарантирует в плане инициализации, потому что нет никаких гарантий, что в этот intent(out)
в конце концов функцией foo()
будет хоть что-то записано. Она может выйти и не трогать этот параметр вообще. А во-вторых, со старым кодом это все работать не будет. В общем, этот гипотетический intent не решает ни одной реальной проблемы, просто очередные шашечки.
Если в функции foo будет путь, в котором выходной параметр не присваивается, компилятор выдаст предупреждение на код этой функции. Точно так же, как выдаёт на функцию, не возвращающую результат.
Со старым кодом, конечно, это всё не работает (и в Фортране тоже), но в целом это полезный механизм.
Если в функции foo будет путь, в котором выходной параметр не присваивается, компилятор выдаст предупреждение на код этой функции.
В очень многих C-like API сделано так, что функция не пишет ничего в свой "выходной" параметр в случае, если произошла ошибка. К примеру, в том же POSIX read()
не обязан инициализировать целевой буфер при возникновении ошибки. Думаю, если начать намеренно ломать такие вещи, многие будут недовольны.
Как раз проконтролировать запись при intent(out) это возможно , аннотации кода уже умеют это делать.
А вот то что старый код останется , это так.
Очередная высосанная из пальца проблема. Как мы жили десятки лет не зная где у нас инициализация в двух строчках кода.
Во наклепали стандартов. Куда вы гоните? Я ещё С++98 не полностью освоил.
Есть компиляторы которые всем глобальным переменным присваивает ноль если мы не сделали свою инициализацию.
Есть компиляторы которые всем глобальным переменным присваивает ноль если мы не сделали свою инициализацию.
Может вы имеете ввиду "локальным переменным", т.к. неинициализированные глобальные переменные по стандарту и так обнулены.
А если такой компилятор локальные переменные обнуляет, тогда где они хранятся в процессе работы? Если на стеке, то при каждом вызове функции их обнулять в стековом фрейме очень затратно будет.
К сожалению, ввод новых стандартов C и C++ не решает проблем, стоящих перед программистами на практике.
На практике, к примеру, у нас на фирме пишется портируемый код на Си. Который предназначен для исполнения на 32-битных микроконтроллерах (несколько архитектур, не только ARM); на Линуксе (в режиме ядра и пользователя) и на Windows (32- и 64-бит).
Ладно, я давно отказался от идеи использовать "новые" стандарты Си, такие как C17 или C11. Остановился на C99 - вроде бы, прошло уже достаточно лет с момента его выпуска, чтобы этот стандарт поддерживался всеми компиляторами для наших целевых платформ. Так и было какое-то время. arm-gcc, Native GCC, MinGW-GCC компилировали код без проблем.
Но недавно возникло два крупных разочарования. 1) MSVC. Нам понадобилось использовать этот компилятор в одном из проектов. А он не поддерживает C99! Нет поддержки комплексных чисел (была важна для проекта). 2) Режим ядра в Линуксе - там обязательно использование C89 для версии ядра 4.x.
В итоге даже 24-летней давности C99 оказалось невозможным использовать.
Сейчас введут какой-нибудь новый C++24, C24 - но боюсь, что и через 20 лет на нем не будет возможно писать реально портируемый код, который поддерживается основной массой компиляторов.
MSVC давно поддерживает C99, и даже C17:
https://learn.microsoft.com/en-us/cpp/overview/visual-cpp-language-conformance?view=msvc-170
Только по этой же ссылке написано, что:
MSVC doesn't support the _Complex keyword or native complex types. The Universal CRT <complex.h> uses implementation-specific macros to achieve the same effect. For more information, see C complex math support.
Насколько я помню, обязательную поддержку комплексных чисел убрали начиная с С11.
Но вообще в MS особо не заморачиваются на этот счет, к примеру, у них long double 64 битный, тогда как раньше был 80.
Да, с C11 комплексные числа являются опциональными. Полагаю это связано с тем, что не все реализовали их в C99.
VLA также опционально не реализован в MSVC, некоторые считают что и в GCC не нужно было реализовывать.
В MSVC Long Double всегда был 64 бит, но это также не является отклонением от стандарта.
Можно реализовать 80-битное число вручную с вызовом ассемблернвх инструкций. Такие реализации уже есть.
И проголосовать: https://developercommunity.visualstudio.com/t/Add-80-bit-or-more-long-double-in-Visual/1533848?entry=myfeedback&space=8
Что значит инициализировать int в C++?