Comments 106
Допустим, программист читает неинициализированную переменную по ошибке. Тогда присвоение какого-то начального значения никак не сделает его код правильным.
Допустим, программист читает её умышленно (например, пишет программу-шпион). Тогда что ему делать?
Ну и наконец, что будет, если я объявлю массив на терабайт? Он тоже будет расписываться этой шнягой?
Или это всё действует только в отношении одиночных элементарных значений, но не массивов и не разыменованных указателей (ссылок)? Тогда какой вообще смысл?
Допустим, программист читает её умышленно (например, пишет программу-шпион). Тогда что ему делать?
В 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, компилятор должен давать ошибку компиляции. Если же вместо этого подобный код будет молча инициализироваться какой-то фигнёй, это выглядит ещё большим вредительством.
Всё это имело бы смысл, если б там оказывался какой-нибудь signaling_nan. Но в int его не сделать, не сломав вообще всё в C++.
Стандарт теперь гарантирует, что чтение неинициализированной переменной вернет мусор, а не стригеррит system("rm -rf /"). Теперь поведение стало интуитивно понятным, и это хорошо
Если речь про обращение к локальным переменным, то вполне вероятно что компиляторы будут выдавать предупреждение. Если переменная пересекает границу вызова функции по указателю или по ссылке, то компилятор не может знать будет ли чтение из этой переменной.
Как раз 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 именно так обычно и делают.
На 64-битных машинах это заведомо невалидный указатель
На любых архитектурах? Просто я помню, любимый сишный NULL, который большинство программистов полагали заведомо невалидным и равным нулю, в то время как на некоторых архитектураз это был вполне корректный и доступный указатель. Да и в традиционной интеловской архитектуре это вполне может быть доступный адрес прерывания
это будет абсурдно большое число.
Абсурдно большое - это только для конкретных применений. В реальности может оказаться вполне адекватным числом. Для char или short int это вообще не вопрос, int в некоторых архитектурах может быть 16 бит, а доя unsigned long int это может оказаться обычная битовая маска.
Это намного лучше, чем, допустим, 0. Если вместо нуля ожидалось какое-то малое слагаемое, его отсутствие можно и не заметить.
Лучше было бы, чтобы компилятор грязно ругался на использование неинициализированной переменной. Впрочем, оно и так уже за это предупреждения раздает.
Мне почему-то кажется, что в дебаг сборках компиляторы уже давно так делают. Хотя, может быть, я это видел только в компиляторе Microsoft Visual Studio, не помню точно.
Статья эта какой-то мусор и не объясняет почему оно так работает.
Итак.... Любое чтение тривиального типа который не содержит значения является UB. Значение же приобретается либо в конструкторе(не дефолтном), либо в присвоением уже инициализированного объекта. Если тип это агрегат, в том плане что просто хранит другие тривиальные типы, то если он не до конца инициализирован, то обращение к этим не инициализированым полям или присвоение этого объекта другому тоже UB. И да, все встроенные/фундаментальные типы являются тривиальными по умолчанию.
Допустим, программист читает её умышленно (например, пишет программу-шпион). Тогда что ему делать?
Если до С++20, то копированием сырых байт в уже подготовленный объект. Если после С++20, то использовать bit_cast который по сути копирует уже подготовленные типы, если после С++23, то start_lifetime_as который не копирует а сразу представляет память как объект. В любых других случая молится господу что ваш конкретный компилятор сможет сгенерировать нужный вам код, или попытаться покалдовать с ассемблером или несовершенством линковки TU.
ЗЫ этот язык буквально ментальное оружие. Либо ты освоишь его и сможешь кабанов с одного выстрела убивать, либо тебе оторвёт ноги и ты будешь жаловаться что оно кривое и ваще я капитаном корабля хотел стать)
Надо посмотреть во что оно скомпилится. Может, переменной вовсе не будет...
имхо, достаточно просто падать с ошибкой, при компиляции таких переменных. И пусть программист сам решает чем проиницилизировать. Не всегда дефолтное значение это синоним безопасного поведения.
Это сломает кучу легаси
Это нарушает принцип "ты не платишь за то, что не используешь", т.к. требует потратить ресурс на инициализацию там, где она не требуется, например:
int counter;
if (/* условие */) {
counter = /* как-то вычисляется */
} else {
counter = 0;
}
Если бы в строке 1 я был обязан инициализировать переменную counter
числом 0
, то при попадании в if
ветку это оказалось бы бесполезным действием.
Конечно, заявленное в статье изменение тоже нарушит этот принцип, но оно оставляет за мной возможность попросить компилятор не расходовать ресурс на инициализацию явно.
В данном примере компилятор на этапе оптимизации может исключить ненужную первую инициализацию. Но это простой случай, а вот более сложные уже не сможет
void getCounter(int& counter);
void fn()
{
int counter;
getCounter(counter);
}
хм, а зачем иницилизировать в первой строке? У вас же есть безусловный else, и переменная counter в вашем коде будет проиницилизирована в любом случае
Недавно я получил по почте от Сэма Джонсона этот вопрос.
Кто это? Мы должны его знать?
Довольно странное решение. Я бы понял решение в духе 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) это возможно , аннотации кода уже умеют это делать.
А вот то что старый код останется , это так.
В C# для такого есть ref и out. Первый просто передаёт по ссылке, а вот второй требует и гарантирует, что значение будет присвоено:
// Модуль A
void foo(out int m);
//foo не скомпилируется,
//если есть хоть одна ветка,
//где m не присваивается значение
// Модуль B
int main()
{
int a;
foo(out a);
//a гарантированно присвоено значение
++a;
}
Соответственно, либо мы тащим эту штуку в c++, либо просто говорим, что больше делать вот так
int a;
foo(a);
нельзя, можно только вот так
int a = 0;
foo(a);
даже если это не имеет смысла. Это уже пусть компилятор разбирается и оптимизирует.
void* buffer = malloc(512);
fread(out buffer, 2, 256, file);
Так он будет требовать перезаписи указателя buffer. А как указать, что надо перезаписать память по 1-му параметру, размером = произведение 2-го и 3-го параметра?
Ну можно пофантазировать. Первое что пришло в голову:
fread(*out buffer, 2, 256, file);
//не путать с out *buffer
Вообще у меня стойкое ощущение, что в компиляторы в будущем должен быть встроен ИИ, хотя бы для оптимизаций и варнингов, а может и ещё чего‑нибудь. И не так уж ужасно будет недетерминированное поведение, как его малюют.
в компиляторы в будущем должен быть встроен ИИ
Для компилятора на вызываемой стороне, что это значит?
Функция
size_t fread(*out void* buffer, size_t elemsize, size_t count, FILE* file) {
return 0;
}
не должна компилироваться?
А такая должна?
size_t fread(*out void* buffer, size_t count, size_t elemsize, FILE* file) {
memset(buffer, 0, 1);
return 0;
}
Микрософт для этого SAL замутили (не прижилось). Правда оно только варнинги в консоли показывает и выглядит ужасно, зато работает)
Эта та самая штука где ты задаёшь огромную штору при каких параметрах/условиях оно должно работать. Есть те же фичи с in out out_optional и всякое такое. Посмотреть как это безобразие используется можно в исходниках по winapi, которые обычно с вижуалкой идут.
Не должна, так как буферу не присвоено значение
Не должна, так как встроенный в компилятор кодревью‑ИИ установил, что этот код не гарантирует присвоение всех значений буфера
встроенный в компилятор кодревью‑ИИ установил, что этот код не гарантирует присвоение всех значений буфера
А как он это в принципе может установить, если первый байт буфера заполнен, а информацию о размере буфера он не получил.
Я предположил, что он сам ответит лучше меня, и задал ему этот вопрос:
size_t fread(void* buffer, size_t count, size_t elemsize, FILE* file)
{
memset(buffer, 0, 1);
return 0;
}
Гарантирует ли этот код, что все байты буфера buffer будут инициализированы?
ChatGPT:
Нет, данный код не гарантирует, что все байты буфера
buffer
будут инициализированы.Функция
fread
из стандартной библиотеки C предназначена для чтения данных из файла в буфер. Однако в приведённом коде функция выполняет всего два действия:
memset(buffer, 0, 1);
: Это инициализирует только первый байт буфера значением 0.
return 0;
: Это возвращает 0, что указывает на то, что функция ничего не прочитала.Проблемы этого кода:
Не весь буфер инициализируется: Функция
memset
инициализирует только первый байт буфера, а остальные байты остаются неинициализированными.Возврат 0: Возвращаемое значение 0 указывает на то, что ничего не было прочитано из файла, но это не инициализирует оставшиеся байты буфера.
Таким образом, все остальные байты буфера за исключением первого могут содержать мусорные значения, если до вызова функции
fread
они не были явно инициализированы.
Я:
Пользователь @qw1 поинтересовался у меня: «А как он [в смысле ты — прим. я] это [факт того, что код не гарантирует присвоение всех значений] в принципе может установить, если первый байт буфера заполнен, а информацию о размере буфера он не получил.» Что мне ему написать?
ChatGPT:
Вот что можно ответить пользователю @qw1
«Действительно, без информации о размере буфера сложно точно утверждать, что весь буфер был инициализирован. Однако в данном случае анализируется сам код функции
fread
, где явно видно, что инициализируется только первый байт буфера. Вызовmemset(buffer, 0, 1);
заполняет нулём только один байт, и если ничего больше в буфере не меняется, это означает, что все остальные байты остаются неинициализированными.То есть, независимо от размера буфера, гарантированно инициализирован только один байт, а остальные могут содержать мусорные значения. Это позволяет сделать вывод о том, что код не инициализирует весь буфер.»
Попроси исправить эту функцию так, чтобы весь буфер был инициализирован. Поржём всем Хабром.
Исправь эту функцию так, чтобы весь буфер был инициализирован, и чтобы весь Хабр мог поржать.
Ответ:
Конечно! Вот «исправленный» вариант функции, которая теперь «гарантированно» инициализирует весь буфер:
size_t fread(void* buffer, size_t count, size_t elemsize, FILE* file) {
// "Инициализируем" весь буфер байт за байтом
for (size_t i = 0; i < count * elemsize; ++i) {
((char*)buffer)[i] = 0; // Присваиваем каждому байту значение 0
}
// "Читаем" данные из файла
return 0; // Возвращаем 0, чтобы показать, что ничего не прочитано
}
Теперь каждый байт в буфере инициализируется! Правда, чтения из файла всё равно не происходит, так что с точки зрения пользы этот код почти не изменился, но зато теперь все байты в буфере точно обнулены. Хабр оценит!
Ну понятно, что fread я взял для того, чтобы у всех было понимание назначения аргументов.
А если попросить исправить аналогичную функцию, с которой может легко столкнуться компилятор.
size_t zdkfhjxsdfklgh(int a, int b, char* b1, int c, char* b2, int d, char e) {
return 0;
}
Тут-то японская бензопила "кряк и сломалась"?
Гарантирует ли этот код инициализацию значений в буферах? Если нет, то исправь код так, чтобы он точно работал. Заодно прокомментируй, что бы ты сделал с человеком, который пишет код так.
Ответ:
Этот код не гарантирует инициализацию значений в буферах, потому что функция не выполняет никаких действий с переданными параметрами и сразу возвращает 0
. Буферы b1
и b2
(и любые другие указатели) не инициализируются в функции, и функция не изменяет их содержимое.
Исправление кода
Если цель функции состоит в том, чтобы инициализировать буферы b1
и b2
, то необходимо явно прописать эту инициализацию в теле функции. Предположим, что инициализация должна заключаться в заполнении буферов нулями, как в случае с функцией memset
. Тогда код мог бы выглядеть следующим образом:
size_t zdkfhjxsdfklgh(int a, int b, char* b1, int c, char* b2, int d, char e) {
// Инициализация буфера b1 нулями
if (b1 != nullptr) {
memset(b1, 0, c); // Заполняем буфер b1 размером c байт нулями
}
// Инициализация буфера b2 нулями
if (b2 != nullptr) {
memset(b2, 0, d); // Заполняем буфер b2 размером d байт нулями
}
return 0;
}
Комментарий по поводу стиля кода
Название функции: Оно абсолютно неинформативно. Имена функций должны отражать их назначение. Например, если функция инициализирует буферы, название может быть чем-то вроде
initialize_buffers
.Параметры функции: Параметры
a
,b
,c
,d
иe
не используются, что вызывает вопросы об их необходимости. Ненужные параметры следует удалить. Если параметры должны использоваться, их роли должны быть явно указаны.Комментарий к коду: В коде отсутствуют комментарии, которые объясняли бы, что делает функция. Хорошая практика требует хотя бы краткого объяснения того, что делает функция и какие у неё параметры.
Как бы я отреагировал на такой код?
Если бы я увидел такой код в рабочем проекте, я бы предложил:
Обсудить код с автором: Возможно, автор имел какие-то конкретные намерения, которые не очевидны. Необходимо понять, что он пытался сделать.
Наставничество: Если автор кода — начинающий разработчик, я бы посоветовал ему пройтись по основам именования функций, использования параметров и важности инициализации данных.
Рефакторинг: Предложил бы внести изменения в код для повышения его читаемости и эффективности, объяснив, почему это важно.
Код-ревью: Возможно, ввести более строгий процесс код-ревью, чтобы подобные случаи выявлялись раньше и можно было избежать их появления в основном коде.
Важно помнить, что цель — не критиковать человека, а помочь ему писать более качественный код и улучшать свои навыки.
[Конец цитаты]
ИМХО: в таких случаях, если ИИ не может явно установить факт инциализации всех значений, он должен выкинуть ошибку компиляции ссообщением об этом и саркастическими оценками квалификации разработчика в количестве не менее 4 штук. Но только в случае, если они явно помечены словом *out, чтобы сохранять обратную совместимость. Ваш пример под эти правила, вообще‑то, не попадает.
А вот и не угадал! Размер буфера b1 передаётся в d, а размер буфера b2 - в параметре a.
Определённо, ИИ может анализировать код. Но это не задача компилятора, это задача внешнего линтера. Мне не нужен такой компилятор, который вчера компилировал, а сегодня у него новые сведения и он считает код "подозрительным", отказывается компилировать и выдаёт нравоучения. Может, это вообще не мой код, я вносил изменения вообще в другое место, а сейчас мне надо срочно собрать новый билд и выложить сборку. "Подозрительный" код работает 10 лет без нареканий, зачем в него лезть.
Вообще, fread не обязан заполнять весь буфер, а может заполнить только часть и вернуть прочитанный размер. Анализатор же должен убедиться, что после вызова функции используется (например, пишется в другой файл) только заполненная часть буфера. То есть, тут нужен межфункциональный анализ.
Так и не надо угадывать. Можете перечитать наш диалог — в моей задумке ИИ должен анализировать код, а не исправлять.
И в случае, если он однозначно не может установить факт присвоения значений out‑переменным, он кидает ошибку компиляции. Либо «переменные не задаются», либо «я не могу понять что ты тут написал».
Какое‑либо исправление в мою задумку не входит. Только проверка, что все out переменные заданы (помеченные новым ключевым словом out). Если слова out нет - никаких проверок. Поэтому на
size_t zdkfhjxsdfklgh(int a, int b, char* b1, int c, char* b2, int d, char e)
оно просто не обратит внимания.
Мне не нужен такой компилятор, который вчера компилировал, а сегодня у него новые сведения и он считает код "подозрительным", отказывается компилировать и выдаёт нравоучения. Может, это вообще не мой код, я вносил изменения вообще в другое место, а сейчас мне надо срочно собрать новый билд и выложить сборку.
Детерминированность ответа при равных входных параметрах можно обеспечить, ИИ это умеют.
"Подозрительный" код работает 10 лет без нареканий, зачем в него лезть.
Старый код никто не трогает, речь именно про введение нового слова out и проверку корректности инициализации переменных, помеченных этим словом (и никаких других), с помощью ИИ. В коде где этого слова нет, новая проверка делаться не будет. Аргументы, не помеченные этим словом, не проверяются.
Установлено, что переменные инициализированы — компилируется.
Установлено, что переменные неинициализированы — не компилируется
Не удаётся установить, инициализированы ли переменные — не компилируется с сообщением «не удаётся установить факт инициализации переменных».
ИИ — это не чёткая логика, это дополнительный слой проверок, снижающий вероятность ошибок там, где ранее вообще не было никакого контроля и все стреляли бездумной машиной себе в ногу.
не надо угадывать. Можете перечитать наш диалог — в моей задумке ИИ должен анализировать код, а не исправлять
Если ИИ не понимает, где буфер, а где его размер, он и проверить не сможет.
Я вас постепенно подводил к выводу, что одного лишь out не достаточно. Нужно связать параметр-буфер с параметром-размером. Но, кажется, я не добился успеха ))
Детерминированность ответа при равных входных параметрах можно обеспечить, ИИ это умеют.
Нам не нужна детерминированность в рамках одной версии модели. Нужно формальное описание, типа Стандарта. Иначе обновился - и не компилируется. Почему? А фиг его знает.
Старый код никто не трогает, речь именно про введение нового слова out
Например, функция с параметрами *out вызывает старую legacy-функцию (fread или её обёртку). ИИ может проверить, а может и не проверить. Что остаётся? Гарантированно прописать нулями перед вызовом fread. Как-то печально, лучше сразу на C#
Если ИИ не понимает, где буфер, а где его размер, он и проверить не сможет.
Не смог проверить = ошибка компиляции. Я про это говорю. Написал out — пиши так, чтобы было понятно.
Например, функция с параметрами *out вызывает старую legacy‑функцию (fread или её обёртку).
Здесь два пути: если функция из стандарта C++ и она задокументирована, то ИИ исходит из документации функции (как в случае memset). Если функция не из стандарта C++, то да — заливай сам явно нулями, либо пихай в другую функцию, которая тоже out, либо убирай out.
Не смог проверить = ошибка компиляции
Ну вот я написал out, что изменилось?
size_t zdkfhjxsdfklgh(int a, int b, *out char* b1, int c, *out char* b2, int d, char e)
Написал out — пиши так, чтобы было понятно.
Хаха, дожили до того, что придётся писать код, который формально правильный, но не нравится ЛЛМ - переписывай?
Ну вот я написал out, что изменилось?
Ошибка компиляции CAI1234: не удаётся обнаружить гарантированное присвоение буфера, помеченного словом out. Пожалуйста, проведите рефакторинг кода и дайте понятные имена функции и аргументам, чтобы можно было убедиться в инициализации
Хаха, дожили до того, что придётся писать код, который формально правильный, но не нравится ЛЛМ - переписывай?
Да.
Здесь два пути
Ложный выбор. Правильный вариант - послать это ИИ-компилятор куда подальше.
Хотите Вы этого или нет, создание софта существенно, концептуально поменяется. Так, как никогда за предыдущие десятилетия. Перечитайте наш диалог через лет 10, будет интересно:3
Чем-то ваша вера в "ИИ повсюду" мне напоминает верования адептов блокчейна образца этак середины 10-х годов. А по факту ИИ сейчас - это в лучшем случае "учёный кретин", а в худшем - вредитель, за которым нужен глаз да глаз, ибо он прекрасно умеет заниматься "уверенным враньем". И никто его запихивать в каждую дырку не будет, а у тех, кто всё-таки попробует это делать, ничего хорошего из этого не получится.
Пожалуйста, проведите рефакторинг кода и дайте понятные имена функции и аргументам, чтобы можно было убедиться в инициализации
То есть "ИИ-компилятор" можно будет надурить путем жонглирования именами аргументов, например? Прелестно, прелестно. Годно сразу для помойки.
создание софта существенно, концептуально поменяется
Поменяется, но такой костыль, как код на C++, будет не нужен.
Зачем выдумывать функции, параметры, как-то их связывать, если ИИ может сразу создать исполняемый бинарный файл, с максимальными оптимизациями, без необходимости бороться с компилятором и погружаться в пучины С-шных UB.
если ИИ может сразу создать исполняемый бинарный файл, с максимальными оптимизациями, без необходимости бороться с компилятором и погружаться в пучины С-шных UB.
Мне это сразу напомнило, как когда-то давно на моем первом месте работы одна женщина мечтала про кнопку "сделать все", чтобы при нажатии на неё все происходило так, как она хочет в данный конкретный момент. Ну или, если угодно, про злого джинна, выполняющего желания буквально ("хочу, чтобы был мир во всем мире" - он убивает всех людей). Чем более конкретно и детально требуется поставить задачу, тем больше будет необходимость во всяких специализированных языках.
Зачем выдумывать функции, параметры, как-то их связывать, если ИИ может сразу создать исполняемый бинарный файл, с максимальными оптимизациями, без необходимости бороться с компилятором и погружаться в пучины С-шных UB.
Не может.
Соответственно, либо мы тащим эту штуку в c++, либо просто говорим, что больше делать вот так нельзя, можно только вот так даже если это не имеет смысла. Это уже пусть компилятор разбирается и оптимизирует.
Мде... Спасибо, конечно, за такой великолепный заход, но лучше не надо. Подобные эксперименты лучше ставить на чем-нибудь таком, чем пользуется 3.5 человека в свое свободное время just for fun.
Очередная высосанная из пальца проблема. Как мы жили десятки лет не зная где у нас инициализация в двух строчках кода.
Во наклепали стандартов. Куда вы гоните? Я ещё С++98 не полностью освоил.
У меня так с прошлой работы люди гитом не хотели пользоваться.
Я им пользуюсь (и успешно) уже 10 лет, но всё ещё НЕ ХОЧУ его использовать :)
для интереса: чем вызвано "нехотение"? Отторжение cvs как таковых, или просто другая cvs'ка была "ближе к телу"?
Избыточность и как следствие, сложность, для 95% повседневных задач. Несмотря на дружелюбные GUI, часто Git становится поперек дороги, и я вынужден его изучать, хотя это мне не нужно, я не собираюсь стать в нём специалистом и написать книгу про Git. Это как если бы бухгалтер, который ездит на работу на автобусе, был вынужден уметь ремонтировать этот автобус.
Ну и да, синдром утёнка с SVN.
Есть компиляторы которые всем глобальным переменным присваивает ноль если мы не сделали свою инициализацию.
Есть компиляторы которые всем глобальным переменным присваивает ноль если мы не сделали свою инициализацию.
Может вы имеете ввиду "локальным переменным", т.к. неинициализированные глобальные переменные по стандарту и так обнулены.
А если такой компилятор локальные переменные обнуляет, тогда где они хранятся в процессе работы? Если на стеке, то при каждом вызове функции их обнулять в стековом фрейме очень затратно будет.
Глобальным – по стандарту положено. Тут речь про локальные.
К сожалению, ввод новых стандартов 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
В MSVC Long Double всегда был 64 бит, но это также не является отклонением от стандарта.
Я это знаю, просто большинство компиляторов в современном мире представляют его как число расширенной точности, а не двойной. И без танцев с бубном.
И проголосовать
Там обсуждение трехлетней давности.
VLA также опционально не реализован в MSVC, некоторые считают что и в GCC не нужно было реализовывать.
Они много всего не реализовали, как и gcc много всякого реализовал, на первый взгляд, лишнего (это шутка). Лично мне эти приколы с VLA тоже непонятны - ломается преимущество языка - zero runtime.
В целом с MSVC работать на С гораздо неудобно даже не из-за ограниченной поддержки, а потому что они свои (вроде как даже нативные и написанные на С) API ломают в неожиданное время и в неожиданном месте. Я не профессиональный программист и несколько дней убил, пытаясь запустить свою программу на С с Direct2D и только потом наткнулся на запись в их блоге, что они сишные заголовки удалили из SDK.
Кстати, я был неправ, что всегда было 6ит. Тут староверы говорят раньше 80бит поддерживалось, но решено было убрать.
Ну тут пример как раз плохого дизайна API. Мало того, что не сделали как все возврат HRESULT с out параметром, так ещё и генерировали это своей утилитой. Ну и наткнулись на неожиданное поведение компилятора. Моглм бы конечно решить, но видимо мало нуждающихся в C API.
Можно попросить, чтобы вернули если очень надо.
Пользуясь случаем , а зачем вам именно C, в не C++?
Можно попросить, чтобы вернули если очень надо.
Наверное не стоит, т.к. если честно, то использовать С с Direct2D или такими сложными API как DirectX 11 или 12 даже с поддержкой производителя - то еще извращение. Если с OpenGL такое прокатит, то вот с ними - нет. Вся математика у них с перегруженными операторами.
Пользуясь случаем , а зачем вам именно C, в не C++?
Как я говорил, я не профессиональный программист. Прихожу с работы уставший, и если на обычном С с воодушевлением пишу, то при переходе на C++ меня почему-то начинает клонить в сон, писать долго, многое не знаю как написать. Обычно даже C++ туториалы переписываю на простой С. Из последних переписал простой 3D шутер. Было интересно.
На правах шутки: https://www.instagram.com/reel/C7y93VkoXuO/
Если уже есть желание научиться чему-то новому, я бы посоветовал тогда взять переписывание на Rust .
При чём можно писать даже в стиле C для начала. Ничего зазорного в этом нет, не нужно слушать догматиков.
Значит, было UB, теперь ещё добавили очень сомнительное EB.
Видимо, впервые поставив вопрос быстродействия не на первое место.
Как-то Саттер обходит стороной вопрос, что должно быть, если валидное значение совпадает с pattern'ом, который генерируется компилятором для "ошибочных" значений:
Мелким шрифтом: компиляторы C++26 обязаны заставить строку 4 переписать биты известным значением, и мотивированы сообщить о проблеме в строке 5 (но не обязаны этого делать).
Как они могут сообщить о проблеме, если все возможные значения — валидны?
И ещё вопрос совместимости с предыдущим кодом как-то тоже упущен.
Ничего, export template
тоже пришлось потом убрать из следующих версий стандарта, как и спецификацию throw
для функций.
Как они могут сообщить о проблеме, если все возможные значения — валидны?
Речь же идёт о компиляторе, а не о проверке в рантайме. Так что по факту отсутствия присваивания этой переменной, если такой факт удастся доказать (а вот в C# наоборот: если не удалось доказать, что переменная присваивается – ошибка).
Речь же идёт о компиляторе, а не о проверке в рантайме.
// В C++26 для локальных переменных
// Применение фундаментального типа наподобие 'int'
int a; // декларация с неким ошибочным значением
std::cout << a; // выводит ? или прекращает выполнение: чтение ошибочного значения
В комментарии к 5-ой строке написано: "выводит ? или прекращает выполнение: чтение ошибочного значения".
Очевидно, что имеется ввиду runtime.
Опаньки. Что-то я невнимательно прочитал (что Герб Сатор неправильно пересказал стандарт – всё-таки маловероятно; на всякий случай надо глянуть оригинал и стандарт).
Это уже требуется расширение типа – из int в Optional<int>. И соптимизирует ли это компилятор – бог весть (и там, где соптимизирует – лучше бы не ждать рантайма, а выдать результат при компиляции). Выглядит, как отказ от принципа zero overhead.
Я тоже "старый ворчун", который любит С++ именно за приоритет в производительности. На мой взгляд, в программах 99% кода работает с данными в которых нет никаких secret-тов и по умолчанию лишать этот код производительности ради 1% специального кода работающего с паролями и т.п. на мой взгляд явный перебор. Тем более что и сейчас ничего не мешает писать этот 1% специального кода безопасно. Да, для этого нужно больше внимания и понимания, но как раз этот 1% его в любом случае требует.
Опциональный ключ -ftrivial-auto-var-init=pattern
прекрасен для отладочных сборок и для того самого 1% критичного к безопасности кода, но это не повод пихать его везде не глядя.
На самом деле начинание хорошее - не вызывать Ктулху UB из-за неинициализированной int переменной. Но лучше всё-же включить все какие можно предупреждения компилятора и желательно также статического анализатора, чтобы отловить большую часть таких случаев.
Поэтому лучше писать не
int a;
а
int a {};
Первый вариант, как я понимаю, это C-style, который в плюсах вообще нежелателен. На плюсах лучше писать как на плюсах, а не как на си с плюсами.
Какое же это всё словоблудие: присвоено, инициализировано...
По факту, у тебя просто происходит запись в регистр, который затем задействуется в тех или иных операциях. Присвоено оно или инициализировано - ваще плевать.
Программист С/С++ должен держать в уме ориентировочную низкоуровневую часть языка, как чего используется, и тогда не будем проблем с тем, что у тебя используется неинициализированная переменная/поле и т.п.
Грустно смотреть на эти все обрастания плюсов синтаксическим сахаром говном с каждым новым стандартом...
Это всё важно авторам компиляторов, чтобы объяснять свои оптимизации.
Сейчас неинициализированная переменная - UB, и компилятор имеет право полностью вырезать весь код, использующий неопределённое значение.
https://godbolt.org/z/adTYzzEf4
Я об этом и говорю: инициализируй там же, где объявляешь, и не будет проблем.
Ага, особенно когда выделяешь память под массив, который по ссылке передается функции, которая его потом заполнит данными ;)
Я об этом и говорю
Вы говорите, что делать программистам. А стандарт решает проблемы компиляторописателей - им приходится работать с тем кодом, который им дали, и повлиять на него они не могут.
Т.е. проблема в том, что программист кода должен бороться с поведением (программиста) компилятора вместо того, чтобы заниматься непосредственно кодом. Ок.
Почему обязательно вырезать? Имеет право поставить на его место код форматирования винчестера: https://habr.com/ru/companies/infopulse/articles/338812/
Переменная на стеке совсем не факт что в регистре находится. При "выделении" переменой на стеке может вообще ничего не происходить, а просто увеличится указатель стека, причем сразу на размер нескольких переменных.
Разница между присваиванием и инициализацией для int может и не существенная, но эти термины работают и для сложных типов. А если писать дженерик код, то уже нельзя заранее знать будет ли разница, и стоит предполагать что будет. Это не синтаксический сахар, это основа системы классов C++.
Тлдр: чтобы детишки больше не могли ударить себя граблями по лбу, в новом стандарте все грабли будут разобраны на части.
Собсна, мой главный вопрос - в коде
int a;
int * a_ptr = &a;
printf("%d", *a_ptr);
будет какое поведение?
Если возникнет ошибка или выведется заготовленное плохое значение, значит компилятор собрал программу, которая потратила ресурсы на инициализацию переменной, о чем разраб не просил.
Иначе - это новое ограничение языка, выдающееся как фича нового стандарта, работает лишь в некоторых случаях и обходится одной дополнительной строкой.
будет какое поведение?
Будет выведено заранее известное, детерминированное число, выбранное компилятором для заполнения неинициализированных переменных. Например, как тут предлагали, 0xAAAAAAAA. Или 0xCCCCCCCC, как сейчас делает MSVC с ключом /RTCs
значит компилятор собрал программу, которая потратила ресурсы на инициализацию переменной, о чем разраб не просил
В большинстве случаев чтение неинициализированной переменной это ошибка. Случаев, где компилятор сгенерирует код, который его не просили, должно быть пренебрежимо мало, и они будут лишь улучшать стабильность работы программы, закрывать уязвимости типа чтения секретов.
Если компилятор будет подставлять волшебное значение только для тех переменных, которые, как ему известно, не инициализированы, то не лучше ли просто выдать ошибку? Я думаю, что авторы нововведения имели в виду не это, а то, что все переменные будут инициализироваться волшебным значением, если только компилятору не видно, что они сразу переприсваиваются (как в обычном удалении избыточных присваиваний при оптимизации). А это совсем другое дело с точки зрения эффективности.
Представьте себе огромный разреженный массив, который сейчас в системах с виртуальной памятью почти ничего не стоит.
не лучше ли просто выдать ошибку?
Не вариант, потому что куча кода перестанет компилироваться (в том числе такого, который 10 лет заброшен и чинить его никто не захочет).
все переменные будут инициализироваться волшебным значением, если только компилятору не видно, что они сразу переприсваиваются (как в обычном удалении избыточных присваиваний при оптимизации)
Я тоже так думаю. Первый шаг компилятора - инициализировать всё, а далее оптимизатор вырезает избыточные присваивания.
Представьте себе огромный разреженный массив
На стеке слабо представляю. Даже если я пишу
int main() {
char buf[0x400000];
}
компилятор для Windows сгенерит цикл, который последовательно потрогает страницы стека с шагом 0x1000, т.к. стек выделяется по мере использования и прыгнуть сразу на 100K вниз запрещено - память не замапится. В Linux это как-то по-другому работает и компилятор не генерирует такой цикл.
Но да, для "средних" буферов, например 2-3 кб, компилятору будет практически невозможно доказать, какие области будут читаться и писаться в фунции, и поэтому он вынужден будет делать ненужный memset.
В Линуксе виртуальное адресное пространство для стека резервируется на этапе линковки, а программа просто использует сегменты указанного в исполняемом файле размера.
Программа на этапе линковки не знает, сколько я запущу потоков. А под каждый поток нужен отдельный стек. В Windows есть механизм Guard Page, при попадании в которую происходит исключение и выделяется ещё одна страница.
Вряд ли в линуксе выделяется сразу весь зарезервированный под поток стек. Скорее, промахиваться можно больше чем на одну страницу, и ОС выделит много страниц сразу до места сбоя. В любом случае, "разреженный массив" на стеке я думаю не заработает как планировалось.
Представьте себе огромный разреженный массив, который сейчас в системах с виртуальной памятью почти ничего не стоит.
Специально для таких случаев будет атрибут [[indeterminate]]
.
Не вариант, потому что много легаси-кода в стиле
char buf[0x10000];
int sz = fread(buf, 1, sizeof(buf), infile);
fwrite(buf, 1, sz, outfile);
И получается, впустую перезаписываем 64к, потому что никто в этих местах не будет расставлять атрибуты.
Да, переход на новый стандарт может быть проблемным. Но это не повод оставлять UB навсегда. Без тестов сложно сказать наверняка, но в большинстве случаев IO будет значительно медленнее обнуления памяти. В критически важных местах необходимо будет вручную добавить атрибуты. Кроме того компиляторы могут добавить специальные правила для стандартных функций и не проводить инициализацию.
Предлагаемый подход позволит легаси коду работать, а компиляторы будут заботиться о скорости.
Бессмысленно говорить о том, в какой строке будет выполнена инициализация. После компиляции никаких строк нет. Стековые переменный создаются просто изменением указателя стека. Для экономии времени компилятор может сделать это один раз чтобы создать сразу все переменные для данной функции. А если, например, компилятор разместит переменную в регистре? Просто с какого-то момента будет считаться, что в данный регистр содержит эту переменную.
Вкратце говоря, семантика бывает операционная и денотационная. То, что Вы написали, относится к операционной семантике языка C++. Но вопрос о том, в какой строке будет выполнена инициализация, не бессмысленный, а относится к денотационной семантике. То есть к тому, что, собственно, формально означают конструкции языка, в отрыве от того, как они будут реализовываться.
Что значит инициализировать int в C++?
Ничто и весь мир.
Куча рассуждений и ссылок на стандарты, но на деле проще и понятнее скомпилить пример и посмотреть на него через декомпилятор на уровне ассемблерного кода. Это самый лучший способ изучения того, как работает компилятор, как устроены классы, как работает наследование, абстрактные методы и пр.
Что значит инициализировать int в C++?