Недавно я получил по почте от Сэма Джонсона этот вопрос. Вот слегка отредактированное письмо Сэма:
«Возьмём для примера этот код в локальной области видимости функции:
int a;
a = 5;
Многие люди считают, что инициализация происходит в строке 1, потому что веб-сайты наподобие cppreference дают такое определение: "Инициализация переменной предоставляет его начальное значение на момент создания".
Однако я убеждён, что инициализация происходит в строке 2, потому что [в разных хороших книгах по C++] инициализация определяется как первое существенное значение, попадающее в переменную.
Можете ли вы сказать, какая строка считается инициализацией?»
Отличный вопрос. На Cppreference написано правильно, и для всех классовых типов ответ прост: объект инициализируется в строке 1 вызовом его стандартного конструктора.
Но (а вы ведь знали, что будет «но») для локального объекта фундаментального встроенного типа наподобие int
ответ будет... чуть более сложным. И именно поэтому Сэм задал этот вопрос, ведь он знает, что язык достаточно свободно обращается с инициализацией таких локальных объектов по историческим причинам, имевшим в то время смысл.
Короткий ответ: вполне допустимо говорить, что переменная получает своё исходное значение в строке 2. Но заметьте, что я намеренно не сказал «Объект инициализируется в строке 2», к тому же и код, и этот ответ обходят молчанием более важный вопрос: «Ну ладно, а что, если код между строками 1 и 2 попробует считать значение объекта?»
Этот пост состоит из трёх частей:
До C++26 ситуация была достаточно неловкой. Но самое забавное то, как это описывается сегодня в Стандарте, ниже я не удержался от цитирования.
В C++26 мы сделали этот код безопасным по умолчанию, благодарить за это стоит Томаса Кёппе! Это был очень важный шаг.
В моём эксперименте Cpp2 эта проблема полностью исчезла, и все типы обрабатываются одинаково, с гарантированной безопасностью инициализации. Я хочу предложить такое решение для самого ISO C++ после C++26, чтобы ISO C++ мог эволюционировать и полностью избавиться от этой проблемы в будущем, если сложится консенсус о внесении такого изменения.
Давайте начнём с современности, со статус-кво, сложившегося до выпуска C++26…
Ответ до C++26: переменная никогда не «инициализируется»
В случае нескольких встроенных типов, например, int
, ответ заключается в том. что в данном примере вообще не происходит инициализации, потому что (строго говоря) ни одна из строк не выполняет инициализацию. Если вас это удивляет, то вот объяснение:
В строке 1 объявляется неинициализированный объект. У него нет начального значения, ни явного, ни косвенного.
Далее в строке 2 присваивается «начальное значение». Эта операция перезаписывает биты объекта биты объекта и присваивает объекту то же значение, что и биты, инициализированные таким образом в строке 1… но это присвоение, а не инициализация (конструкция).
Тем не менее, я думаю, разумно будет неформально назвать строку 2 «заданием начального значения» в том смысле, что это записывание в этот объект первого существенного для программы значения. С формальной точки зрения это не инициализация, но в конечном итоге биты становятся одинаковыми, и в хороших книгах строку 2 могут резонно называть «инициализацией a
».
«Но постойте-ка», — может сказать кто-то. «Вчера вечером я читал Стандарт, и в [dcl.init] говорится, что строка 1 — это и есть "инициализация значением по умолчанию"! То есть строка 1 и есть инициализация!» На эти утверждения я могу ответить «да» и «нет». Давайте же взглянем на формальный точный и довольно забавный ответ из Стандарта, он просто великолепен: Стандарт действительно гласит, что в строке 1 объект инициализируется значением по умолчанию… но, для типов наподобие int
, термин «инициализируется значением по умолчанию» обозначает «инициализация не выполняется».
Я это не придумал, см. параграф 7 [dcl.init].
(Самое время сказать: «Стандарт — это не туториал»... Иными словами, не стоит читать Стандарт для изучения языка. Стандарт достаточно чётко описывает действия C++, и нет ничего плохого в том, что он определяет всё таким образом, это совершенно нормально. Но он не написан для обывателя, и никто не обвинит вас, если вы подумаете, что «инициализация значением по умолчанию означает отсутствие инициализации» — это пример когнитивного диссонанса, оруэлловского двоемыслия (это не одно и то же) или пассивно-агрессивной провокации.)
Можно задать близкий этому вопрос: началось ли время жизни объекта после строки 1? Хорошие новости заключаются в том, что да, в строке 1 действительно началось время жизни неинициализированного объекта, согласно параграфу 1 [basic.life]. Но давайте не будем слишком вдаваться в разбор фразы о «пустой инициализации» из этого параграфа, потому что это ещё одно иносказание Стандарта той же концепции «это инициализация, хотя нет, мы просто пошутили». (Я ведь уже говорил, что Стандарт — это не туториал?) И, разумеется, это серьёзная проблема, ведь время жизни объекта уже началось, но он ещё не инициализирован предсказуемым значением. Это наихудшая проблема неинициализированной переменной, ведь считывание из неё может представлять угрозу для безопасности; это настоящее «неопределённое поведение», способное на что угодно, и нападающие могут использовать это свойство.
К счастью, в C++26 ситуация с безопасностью становится намного лучше…
C++26: всё становится лучше (на самом деле) и безопасным по умолчанию
Всего несколько месяцев назад (в марте 2024 года, на совещании в Токио) мы улучшили эту ситуацию в C++26, внедрив статью Томаса Кёппе P2795R5, «Erroneous behavior for uninitialized reads». Возможно, её название может показаться знакомым для читателей моего блога, ведь я упоминал её в своём отчёте о поездке в Токио.
В C++26 была создана новая концепция ошибочного поведения (erroneous behavior), которая лучше «неопределённого» или «неуточнённого», ведь она позволяет нам рассуждать о коде «который точно определён как ошибочный» (серьёзно, это почти прямая цитата из статьи), а поскольку код теперь точно определён, мы избавляемся от угрозы безопасности, связанной с «неопределённым поведением». Можно воспринимать это как инструмент Стандарта, позволяющий превратить некое поведение из «пугающе неопределённого» в «что ж, частично это наша вина, потому что мы позволили вам написать этот код, который значит не то, что должен значить, но на самом деле вы написали здесь баг, и мы поставим ограждение вокруг этой ямы с кольями, чтобы по умолчанию вы в неё не падали». И впервые эта концепция была применена к... барабанная дробь... неинициализированным локальным переменным.
И это очень важно, потому что означает, что строка 1 из исходного примера по-прежнему не инициализирована, но начиная с C++26 это становится «ошибочным поведением», то есть при сборке кода компилятором C++26 неопределённое поведение не может возникнуть при чтении неинициализированного значения. Да, из этого следует, что компилятор C++26 будет генерировать отличающийся от предыдущего код... Он гарантировано запишет известное компилятору ошибочное значение (но это не гарантирует, что на него может положиться программист, так что к нему по-прежнему ноль доверия), если есть хоть какая-то вероятность, что значение могут считать.
Это кажется несущественным, но на самом деле это важное улучшение, доказывающее, что комитет серьёзно настроен на активное изменение нашего языка в сторону его безопасности по умолчанию. Тенденцию увеличения объёмов безопасного по умолчанию кода мы будем наблюдать в ближайшем будущем C++, и это можно только приветствовать.
Пока вы ждёте, что ваш любимый компилятор C++26 добавит поддержку этого, можно получить аппроксимацию этой функции при помощи переключателя GCC или Clang -ftrivial-auto-var-init=pattern
или при помощи переключателя MSVC /RTC1
(поторопитесь использовать их, если можете). Они дадут вам практически всё то, что даст C++26, за исключением, возможно, того, что не будут создавать диагностику (например, переключатель Clang создаёт диагностику, только если запустить Memory Sanitizer).
Например, рассмотрим, как это новое поведение по умолчанию препятствует утеканию секретов, на примере программы, скомпилированной с сегодняшним флагом и без него (ссылка на Godbolt):
template<int N>
auto print(char (&a)[N]) { std::cout << std::string_view{a,N} << "\n"; }
auto f1() {
char a[] = {'s', 'e', 'c', 'r', 'e', 't' };
print(a);
}
auto f2() {
char a[6];
print(a); // сегодня этот код, вероятно, выведет "secret"
}
auto f3() {
char a[] = {'0', '1', '2', '3', '4', '5' };
print(a); // перезаписывает "secret"
}
int main() {
f1();
f2();
f3();
}
Стандартно все три локальных массива используют одно и то же стековое хранилище, и после того, как f1
вернёт строку secret
, она, вероятно, всё ещё будет находиться в стеке, ожидая, что на неё наложится массив f2
.
В сегодняшнем C++ по умолчанию без -ftrivial-auto-var-init=pattern
или /RTC1
функция f2
, вероятно, выведет secret
. Что может вызвать, скажем так, проблемы безопасности и защиты. Такое неопределённое поведение правила отсутствия инициализации и создаёт плохую репутацию C++.
Но при использовании -ftrivial-auto-var-init=pattern
компиляторов GCC и Clang или /RTC1
компилятора MSVC , а также начиная с C++26 и далее по умолчанию функция f2
не приведёт к утечке секрета. Как иногда говорит Бьёрн в других контекстах, «Это прогресс!» А тем ворчунам, кто, возможно, хотел бы сказать: «Автор, я привык к небезопасному коду, избавление от небезопасного кода по умолчанию противоречит духу C++», отвечу, что (а) таково настоящее и (б) привыкайте к этому, потому что подобного в дальнейшем будет намного больше.
Дополнение: часто задают вопрос о том, почему бы не инициализировать переменную значением 0? Это предлагают постоянно, но это не лучший ответ по многим причинам. Вот две основные: (1) ноль не всегда бывает существенным для программы значением, так что инъецирование его часто приводит к замене одного бага другим; (2) часто он активно маскирует от санитайзеров сбои инициализации, поэтому мы не можем увидеть ошибку и сообщить о ней. Использование определённого реализацией хорошо известного «ошибочного» битового паттерна не приводит к таким проблемам.
Но это ведь C++, так что вы всегда можете при необходимости взять полный контроль в свои руки и получить максимальную производительность. Так что да, при сильном желании C++26 позволяет отказаться от этого, написав [[indeterminate]]
, но каждое использование этого атрибута должно подвергаться проверке при каждом ревью кода и иметь чёткое оправдание в виде точных измерений производительности, демонстрирующих необходимость переопределения безопасного поведения по умолчанию:
int a [[indeterminate]] ;
// Так в C++26 можно сказать "да, пожалуйста, сделай мне больно,
// мне нужна эта старая опасная семантика"
После C++26: что ещё мы можем сделать?
Вот, какая у нас ситуация до C++26 (самые проблемные строки — 4 и 5):
// В современном C++ до C++26 для локальных переменных
// Применение фундаментального типа наподобие 'int'
int a; // объявление без инициализации
std::cout << a; // неопределённое поведение: чтение неинициализированной переменной
a = 5; // присвоение (не инициализация)
std::cout << a; // выводит 5
// Применение классового типа наподобие 'std::string'
string b; // объявление с конструкцией по умолчанию
std::cout << b; // выводит "": чтение сконструированного по умолчанию значения
b = "5"; // присвоение (не инициализация)
std::cout << b; // выводит "5"
Стоит отметить, что строка 5 может и ничего не выводить… это неопределённое поведение, так что вам повезёт, если вопрос будет только в выводе и не выводе, ведь соответствующий стандартам компилятор, теоретически, может сгенерировать код, стирающий жёсткий диск, вызывающий nasal demons или приводящий к другим традиционным проказам неопределённого поведения.
А вот, с чего мы начинаем в C++26 (отличия находятся в строках 4 и 5):
// В C++26 для локальных переменных
// Применение фундаментального типа наподобие 'int'
int a; // декларация с неким ошибочным значением
std::cout << a; // выводит ? или прекращает выполнение: чтение ошибочного значения
a = 5; // присвоение (не инициализация)
std::cout << a; // выводит 5
// Применение классового типа наподобие 'std::string'
string b; // объявление с конструкцией по умолчанию
std::cout << b; // выводит "": чтение сконструированного по умолчанию значения
b = "5"; // присвоение (не инициализация)
std::cout << b; // выводит "5"
Хорошие новости: теперь наши жёсткие диски в безопасности: реализация может вывести значение или прервать выполнение, но неопределённого поведения не будет.
Мелким шрифтом: компиляторы C++26 обязаны заставить строку 4 переписать биты известным значением, и мотивированы сообщить о проблеме в строке 5 (но не обязаны этого делать).
В моём экспериментальном синтаксисе Cpp2 локальные переменные всех типов определяются так: a: some_type = initial_value;
. Можно опустить часть с = initial_value
, чтобы дать понять, что пространство стека выделено под переменную, но сама её инициализация отложена, после чего Cpp2 гарантирует инициализацию до использования; вы обязаны выполнить инициализацию позже при помощи =
(например, a = initial_value;
), прежде чем как-то использовать переменную, что обеспечивает нам гибкость, например, позволяет использовать разные конструкторы для одной и той же переменной по разным путям ветвления. То есть эквивалентный пример будет таким (отличия от C++26 находятся в строках 4-6 и 10-12):
// Локальные переменные в моём синтаксисе Cpp2
// Применение фундаментального типа наподобие 'int'
a: int; // выделяет пространство, без инициализации
// std::cout << a; // недопустимо: нельзя использовать до инициализации!
a = 5; // конструкция => реальная инициализация!
std::cout << a; // выводит 5
// Применение классового типа наподобие 'std::string'
b: string; // выделяет пространство, без инициализации
// std::cout << b; // недопустимо: нельзя использовать до инициализации!
b = "5"; // конструкция => реальная инициализация!
std::cout << b; // выводит "5"
В Cpp2 намеренно не оставлено простых способов отказаться от такой схемы и использовать переменную до её инициализации. Чтобы добиться этого, нужно создать в стеке массив сырых std::byte
или что-то подобное, а затем выполнить unsafe_cast
, чтобы притвориться, что это другой тип... Писать это длинно и сложно, ведь я считаю, что небезопасный код должен быть длинным и сложным в написании… но его можно при необходимости написать, потому что такова природа C++: я могу осуждать небезопасный код, который вы захотите написать ради производительности, но я до смерти буду защищать ваше право писать его при необходимости; C++ всегда позволяет залезть внутрь и взять управление на себя. Я стремлюсь перейти от модели «производительность по умолчанию, безопасность всегда доступна», в которой для обеспечения безопасности нужно прикладывать дополнительные усилия, к модели «безопасность по умолчанию, производительность всегда доступна». Я придумал для этого такую метафору: мне не хочется отбирать у программистов на C++ острые ножи, потому что шеф-поварам иногда нужны острые ножи; но когда ножами не пользуются, мы просто хотим положить их в ящик, который нужно осознанно открывать, а не разбрасывать их по полу и постоянно напоминать людям, чтобы они смотрели под ноги.
Пока эта модель работает очень хорошо и обладает тройным преимуществом: производительность (инициализация не выполняется, пока вам это не нужно), гибкость (можно вызвать тот реальный конструктор, который мне нужен), безопасность (реальная «инициализация» с реальной конструкцией и никогда не возникает ситуации использования до инициализации). Думаю, когда-нибудь это может появиться и в ISO C++, и я намерен через год-два отправить предложение в этом стиле комитету ISO C++, сделав его максимально убедительным. Возможно, комитету оно понравится, или же он найдёт незамеченные мной недостатки... Посмотрим! Как бы то ни было, я буду сообщать о новостях в своём блоге.
Ещё раз благодарю Сэма Джонсона за этот вопрос!
Комментарии (102)
vilgeforce
23.08.2024 08:53Надо посмотреть во что оно скомпилится. Может, переменной вовсе не будет...
viordash
23.08.2024 08:53+4имхо, достаточно просто падать с ошибкой, при компиляции таких переменных. И пусть программист сам решает чем проиницилизировать. Не всегда дефолтное значение это синоним безопасного поведения.
ExternalWayfarer
23.08.2024 08:53+6Недавно я получил по почте от Сэма Джонсона этот вопрос.
Кто это? Мы должны его знать?
aamonster
23.08.2024 08:53+4Довольно странное решение. Я бы понял решение в духе C# – когда на всех путях переменная до чтения гарантированно должна быть присвоена, иначе программа не скомпилируется, но присваисвание ей какого-то неизвестного значения (0xDEADBEEF, что ли?) не выглядит серьёзным улучшением по сравнению с мусором из стека.
KanuTaH
23.08.2024 08:53+5Я бы понял решение в духе C# – когда на всех путях переменная до чтения гарантированно должна быть присвоена, иначе программа не скомпилируется
Это нереально. Простой пример:
// Модуль A void foo(int&); // Модуль B int main() { int a; foo(a); ++a; }
Предположим что модуль A распространяется в виде уже скомпилированной библиотеки (DLL или .so). Откуда компилятор знает, что делает функция
foo()
? Инициализирует ли она переменнуюa
? А может быть наоборот, она из нее только читает? Он не может быть в этом уверен. Если в подобных случаях заставлять погромиста явно инициализироватьa
до вызоваfoo()
, то нужно будет править огромное количество существующего кода, который совершенно нормально работает, чисто для того, чтобы удовлетворить компилятор. Никто на это не пойдет.но присваисвание ей какого-то неизвестного значения (0xDEADBEEF, что ли?) не выглядит серьёзным улучшением по сравнению с мусором из стека.
Дело не в "мусоре из стека", а в том, что сейчас компилятор считает, что чтение неинициализированной переменной просто не может происходить. И делает разнообразные оптимизации (например, выкидывание "лишнего" по его мнению кода) исходя из этого. После введения erroneous behavior компилятор уже будет в курсе, что такое чтение происходить может, просто это ненормально. И будет заниматься преобразованием и оптимизацией кода исходя уже из новых представлений.
aamonster
23.08.2024 08:53Да я знаю про оптимизации. И понятно, что компилятор теряет возможность проводить оптимизации, полагаясь на то, что программист не будет читать переменную до записи в неё. Т.е. возможный выигрыш в быстродействии потеряли, а от ошибок не избавились.
KanuTaH
23.08.2024 08:53+1От ошибок в том виде, как это сделано в C# (на стадии компиляции), тут и невозможно избавиться. Можно лишь облегчить их последствия.
vadimr
23.08.2024 08:53Конкретно в приведённом листинге проблема только в том, что в синтаксисе C++ не указывается направление передачи параметров.
В каком-нибудь Фортране, если параметр описан как
intent(in)
илиintent(inout)
, то это некорректный код, а если какintent(out)
– то корректный. И компилятор, соответственно, это сечёт.Лучше б в C++ добавили intent.
KanuTaH
23.08.2024 08:53Лучше б в C++ добавили intent.
Даже если предположить, что кто-то решит сделать нечто подобное, то, во-первых, это ничего не гарантирует в плане инициализации, потому что нет никаких гарантий, что в этот
intent(out)
в конце концов функциейfoo()
будет хоть что-то записано. Она может выйти и не трогать этот параметр вообще. А во-вторых, со старым кодом это все работать не будет. В общем, этот гипотетический intent не решает ни одной реальной проблемы, просто очередные шашечки.vadimr
23.08.2024 08:53Если в функции foo будет путь, в котором выходной параметр не присваивается, компилятор выдаст предупреждение на код этой функции. Точно так же, как выдаёт на функцию, не возвращающую результат.
Со старым кодом, конечно, это всё не работает (и в Фортране тоже), но в целом это полезный механизм.
KanuTaH
23.08.2024 08:53+1Если в функции foo будет путь, в котором выходной параметр не присваивается, компилятор выдаст предупреждение на код этой функции.
В очень многих C-like API сделано так, что функция не пишет ничего в свой "выходной" параметр в случае, если произошла ошибка. К примеру, в том же POSIX
read()
не обязан инициализировать целевой буфер при возникновении ошибки. Думаю, если начать намеренно ломать такие вещи, многие будут недовольны.
NN1
23.08.2024 08:53Как раз проконтролировать запись при intent(out) это возможно , аннотации кода уже умеют это делать.
А вот то что старый код останется , это так.
VBDUnit
23.08.2024 08:53+2В 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);
даже если это не имеет смысла. Это уже пусть компилятор разбирается и оптимизирует.
qw1
23.08.2024 08:53+1void* buffer = malloc(512); fread(out buffer, 2, 256, file);
Так он будет требовать перезаписи указателя buffer. А как указать, что надо перезаписать память по 1-му параметру, размером = произведение 2-го и 3-го параметра?
VBDUnit
23.08.2024 08:53Ну можно пофантазировать. Первое что пришло в голову:
fread(*out buffer, 2, 256, file); //не путать с out *buffer
Вообще у меня стойкое ощущение, что в компиляторы в будущем должен быть встроен ИИ, хотя бы для оптимизаций и варнингов, а может и ещё чего‑нибудь. И не так уж ужасно будет недетерминированное поведение, как его малюют.
KanuTaH
23.08.2024 08:53в компиляторы в будущем должен быть встроен ИИ
VBDUnit
23.08.2024 08:53Ну, я осознаю чудовищные риски внедрения ИИ, но это всё равно будет сделано кем‑то. Так что лучше уж пусть это внедрение будет контролируемо, чем когда все договорятся этого не делать, а потом побегут подпольно делать. Из страха, что остальные могут это сделать подпольно.
qw1
23.08.2024 08:53Для компилятора на вызываемой стороне, что это значит?
Функция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; }
yatanai
23.08.2024 08:53Микрософт для этого SAL замутили (не прижилось). Правда оно только варнинги в консоли показывает и выглядит ужасно, зато работает)
Эта та самая штука где ты задаёшь огромную штору при каких параметрах/условиях оно должно работать. Есть те же фичи с in out out_optional и всякое такое. Посмотреть как это безобразие используется можно в исходниках по winapi, которые обычно с вижуалкой идут.
VBDUnit
23.08.2024 08:53Не должна, так как буферу не присвоено значение
Не должна, так как встроенный в компилятор кодревью‑ИИ установил, что этот код не гарантирует присвоение всех значений буфера
qw1
23.08.2024 08:53встроенный в компилятор кодревью‑ИИ установил, что этот код не гарантирует присвоение всех значений буфера
А как он это в принципе может установить, если первый байт буфера заполнен, а информацию о размере буфера он не получил.
VBDUnit
23.08.2024 08:53Я предположил, что он сам ответит лучше меня, и задал ему этот вопрос:
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);
заполняет нулём только один байт, и если ничего больше в буфере не меняется, это означает, что все остальные байты остаются неинициализированными.То есть, независимо от размера буфера, гарантированно инициализирован только один байт, а остальные могут содержать мусорные значения. Это позволяет сделать вывод о том, что код не инициализирует весь буфер.»
qw1
23.08.2024 08:53Попроси исправить эту функцию так, чтобы весь буфер был инициализирован. Поржём всем Хабром.
VBDUnit
23.08.2024 08:53+2Исправь эту функцию так, чтобы весь буфер был инициализирован, и чтобы весь Хабр мог поржать.
Ответ:
Конечно! Вот «исправленный» вариант функции, которая теперь «гарантированно» инициализирует весь буфер:
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, чтобы показать, что ничего не прочитано }
Теперь каждый байт в буфере инициализируется! Правда, чтения из файла всё равно не происходит, так что с точки зрения пользы этот код почти не изменился, но зато теперь все байты в буфере точно обнулены. Хабр оценит!
qw1
23.08.2024 08:53Ну понятно, что fread я взял для того, чтобы у всех было понимание назначения аргументов.
А если попросить исправить аналогичную функцию, с которой может легко столкнуться компилятор.size_t zdkfhjxsdfklgh(int a, int b, char* b1, int c, char* b2, int d, char e) { return 0; }
Тут-то японская бензопила "кряк и сломалась"?
VBDUnit
23.08.2024 08:53Гарантирует ли этот код инициализацию значений в буферах? Если нет, то исправь код так, чтобы он точно работал. Заодно прокомментируй, что бы ты сделал с человеком, который пишет код так.
Ответ:
Этот код не гарантирует инициализацию значений в буферах, потому что функция не выполняет никаких действий с переданными параметрами и сразу возвращает
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, чтобы сохранять обратную совместимость. Ваш пример под эти правила, вообще‑то, не попадает.
qw1
23.08.2024 08:53+1А вот и не угадал! Размер буфера b1 передаётся в d, а размер буфера b2 - в параметре a.
Определённо, ИИ может анализировать код. Но это не задача компилятора, это задача внешнего линтера. Мне не нужен такой компилятор, который вчера компилировал, а сегодня у него новые сведения и он считает код "подозрительным", отказывается компилировать и выдаёт нравоучения. Может, это вообще не мой код, я вносил изменения вообще в другое место, а сейчас мне надо срочно собрать новый билд и выложить сборку. "Подозрительный" код работает 10 лет без нареканий, зачем в него лезть.
Вообще, fread не обязан заполнять весь буфер, а может заполнить только часть и вернуть прочитанный размер. Анализатор же должен убедиться, что после вызова функции используется (например, пишется в другой файл) только заполненная часть буфера. То есть, тут нужен межфункциональный анализ.
VBDUnit
23.08.2024 08:53Так и не надо угадывать. Можете перечитать наш диалог — в моей задумке ИИ должен анализировать код, а не исправлять.
И в случае, если он однозначно не может установить факт присвоения значений out‑переменным, он кидает ошибку компиляции. Либо «переменные не задаются», либо «я не могу понять что ты тут написал».
Какое‑либо исправление в мою задумку не входит. Только проверка, что все out переменные заданы (помеченные новым ключевым словом out). Если слова out нет - никаких проверок. Поэтому на
size_t zdkfhjxsdfklgh(int a, int b, char* b1, int c, char* b2, int d, char e)
оно просто не обратит внимания.
Мне не нужен такой компилятор, который вчера компилировал, а сегодня у него новые сведения и он считает код "подозрительным", отказывается компилировать и выдаёт нравоучения. Может, это вообще не мой код, я вносил изменения вообще в другое место, а сейчас мне надо срочно собрать новый билд и выложить сборку.
Детерминированность ответа при равных входных параметрах можно обеспечить, ИИ это умеют.
"Подозрительный" код работает 10 лет без нареканий, зачем в него лезть.
Старый код никто не трогает, речь именно про введение нового слова out и проверку корректности инициализации переменных, помеченных этим словом (и никаких других), с помощью ИИ. В коде где этого слова нет, новая проверка делаться не будет. Аргументы, не помеченные этим словом, не проверяются.
Установлено, что переменные инициализированы — компилируется.
Установлено, что переменные неинициализированы — не компилируется
Не удаётся установить, инициализированы ли переменные — не компилируется с сообщением «не удаётся установить факт инициализации переменных».
ИИ — это не чёткая логика, это дополнительный слой проверок, снижающий вероятность ошибок там, где ранее вообще не было никакого контроля и все стреляли бездумной машиной себе в ногу.
qw1
23.08.2024 08:53не надо угадывать. Можете перечитать наш диалог — в моей задумке ИИ должен анализировать код, а не исправлять
Если ИИ не понимает, где буфер, а где его размер, он и проверить не сможет.
Я вас постепенно подводил к выводу, что одного лишь out не достаточно. Нужно связать параметр-буфер с параметром-размером. Но, кажется, я не добился успеха ))
Детерминированность ответа при равных входных параметрах можно обеспечить, ИИ это умеют.
Нам не нужна детерминированность в рамках одной версии модели. Нужно формальное описание, типа Стандарта. Иначе обновился - и не компилируется. Почему? А фиг его знает.
Старый код никто не трогает, речь именно про введение нового слова out
Например, функция с параметрами *out вызывает старую legacy-функцию (fread или её обёртку). ИИ может проверить, а может и не проверить. Что остаётся? Гарантированно прописать нулями перед вызовом fread. Как-то печально, лучше сразу на C#
VBDUnit
23.08.2024 08:53Если ИИ не понимает, где буфер, а где его размер, он и проверить не сможет.
Не смог проверить = ошибка компиляции. Я про это говорю. Написал out — пиши так, чтобы было понятно.
Например, функция с параметрами *out вызывает старую legacy‑функцию (fread или её обёртку).
Здесь два пути: если функция из стандарта C++ и она задокументирована, то ИИ исходит из документации функции (как в случае memset). Если функция не из стандарта C++, то да — заливай сам явно нулями, либо пихай в другую функцию, которая тоже out, либо убирай out.
qw1
23.08.2024 08:53Не смог проверить = ошибка компиляции
Ну вот я написал out, что изменилось?
size_t zdkfhjxsdfklgh(int a, int b, *out char* b1, int c, *out char* b2, int d, char e)
Написал out — пиши так, чтобы было понятно.
Хаха, дожили до того, что придётся писать код, который формально правильный, но не нравится ЛЛМ - переписывай?
VBDUnit
23.08.2024 08:53Ну вот я написал out, что изменилось?
Ошибка компиляции CAI1234: не удаётся обнаружить гарантированное присвоение буфера, помеченного словом out. Пожалуйста, проведите рефакторинг кода и дайте понятные имена функции и аргументам, чтобы можно было убедиться в инициализации
Хаха, дожили до того, что придётся писать код, который формально правильный, но не нравится ЛЛМ - переписывай?
Да.
qw1
23.08.2024 08:53Здесь два пути
Ложный выбор. Правильный вариант - послать это ИИ-компилятор куда подальше.
VBDUnit
23.08.2024 08:53Хотите Вы этого или нет, создание софта существенно, концептуально поменяется. Так, как никогда за предыдущие десятилетия. Перечитайте наш диалог через лет 10, будет интересно:3
KanuTaH
23.08.2024 08:53+1Чем-то ваша вера в "ИИ повсюду" мне напоминает верования адептов блокчейна образца этак середины 10-х годов. А по факту ИИ сейчас - это в лучшем случае "учёный кретин", а в худшем - вредитель, за которым нужен глаз да глаз, ибо он прекрасно умеет заниматься "уверенным враньем". И никто его запихивать в каждую дырку не будет, а у тех, кто всё-таки попробует это делать, ничего хорошего из этого не получится.
Пожалуйста, проведите рефакторинг кода и дайте понятные имена функции и аргументам, чтобы можно было убедиться в инициализации
То есть "ИИ-компилятор" можно будет надурить путем жонглирования именами аргументов, например? Прелестно, прелестно. Годно сразу для помойки.
qw1
23.08.2024 08:53создание софта существенно, концептуально поменяется
Поменяется, но такой костыль, как код на C++, будет не нужен.
Зачем выдумывать функции, параметры, как-то их связывать, если ИИ может сразу создать исполняемый бинарный файл, с максимальными оптимизациями, без необходимости бороться с компилятором и погружаться в пучины С-шных UB.
KanuTaH
23.08.2024 08:53если ИИ может сразу создать исполняемый бинарный файл, с максимальными оптимизациями, без необходимости бороться с компилятором и погружаться в пучины С-шных UB.
Мне это сразу напомнило, как когда-то давно на моем первом месте работы одна женщина мечтала про кнопку "сделать все", чтобы при нажатии на неё все происходило так, как она хочет в данный конкретный момент. Ну или, если угодно, про злого джинна, выполняющего желания буквально ("хочу, чтобы был мир во всем мире" - он убивает всех людей). Чем более конкретно и детально требуется поставить задачу, тем больше будет необходимость во всяких специализированных языках.
eptr
23.08.2024 08:53Зачем выдумывать функции, параметры, как-то их связывать, если ИИ может сразу создать исполняемый бинарный файл, с максимальными оптимизациями, без необходимости бороться с компилятором и погружаться в пучины С-шных UB.
Не может.
KanuTaH
23.08.2024 08:53+1Соответственно, либо мы тащим эту штуку в c++, либо просто говорим, что больше делать вот так нельзя, можно только вот так даже если это не имеет смысла. Это уже пусть компилятор разбирается и оптимизирует.
Мде... Спасибо, конечно, за такой великолепный заход, но лучше не надо. Подобные эксперименты лучше ставить на чем-нибудь таком, чем пользуется 3.5 человека в свое свободное время just for fun.
rukhi7
23.08.2024 08:53+13Очередная высосанная из пальца проблема. Как мы жили десятки лет не зная где у нас инициализация в двух строчках кода.
yrHeTaTeJlb
23.08.2024 08:53+1Не жили богато, нечего и начинать
То что в C++ ub на каждом шагу это не высосанная из пальца проблема. Чем меньше в коде мест, которые могут скомпилироваться в system("rm -rf /"), тем лучше
ForestDront
23.08.2024 08:53+5Во наклепали стандартов. Куда вы гоните? Я ещё С++98 не полностью освоил.
Lainhard
23.08.2024 08:53У меня так с прошлой работы люди гитом не хотели пользоваться.
gyzl
23.08.2024 08:53Я им пользуюсь (и успешно) уже 10 лет, но всё ещё НЕ ХОЧУ его использовать :)
qrKot
23.08.2024 08:53для интереса: чем вызвано "нехотение"? Отторжение cvs как таковых, или просто другая cvs'ка была "ближе к телу"?
gyzl
23.08.2024 08:53Избыточность и как следствие, сложность, для 95% повседневных задач. Несмотря на дружелюбные GUI, часто Git становится поперек дороги, и я вынужден его изучать, хотя это мне не нужно, я не собираюсь стать в нём специалистом и написать книгу про Git. Это как если бы бухгалтер, который ездит на работу на автобусе, был вынужден уметь ремонтировать этот автобус.
Ну и да, синдром утёнка с SVN.
AVKinc
23.08.2024 08:53Есть компиляторы которые всем глобальным переменным присваивает ноль если мы не сделали свою инициализацию.
Serpentine
23.08.2024 08:53Есть компиляторы которые всем глобальным переменным присваивает ноль если мы не сделали свою инициализацию.
Может вы имеете ввиду "локальным переменным", т.к. неинициализированные глобальные переменные по стандарту и так обнулены.
А если такой компилятор локальные переменные обнуляет, тогда где они хранятся в процессе работы? Если на стеке, то при каждом вызове функции их обнулять в стековом фрейме очень затратно будет.
MichaelBorisov
23.08.2024 08:53+3К сожалению, ввод новых стандартов 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 лет на нем не будет возможно писать реально портируемый код, который поддерживается основной массой компиляторов.
NN1
23.08.2024 08:53MSVC давно поддерживает C99, и даже C17:
https://learn.microsoft.com/en-us/cpp/overview/visual-cpp-language-conformance?view=msvc-170
Serpentine
23.08.2024 08:53+2Только по этой же ссылке написано, что:
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.
NN1
23.08.2024 08:53Да, с 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
Serpentine
23.08.2024 08:53+2В MSVC Long Double всегда был 64 бит, но это также не является отклонением от стандарта.
Я это знаю, просто большинство компиляторов в современном мире представляют его как число расширенной точности, а не двойной. И без танцев с бубном.
И проголосовать
Там обсуждение трехлетней давности.
VLA также опционально не реализован в MSVC, некоторые считают что и в GCC не нужно было реализовывать.
Они много всего не реализовали, как и gcc много всякого реализовал, на первый взгляд, лишнего (это шутка). Лично мне эти приколы с VLA тоже непонятны - ломается преимущество языка - zero runtime.
В целом с MSVC работать на С гораздо неудобно даже не из-за ограниченной поддержки, а потому что они свои (вроде как даже нативные и написанные на С) API ломают в неожиданное время и в неожиданном месте. Я не профессиональный программист и несколько дней убил, пытаясь запустить свою программу на С с Direct2D и только потом наткнулся на запись в их блоге, что они сишные заголовки удалили из SDK.
NN1
23.08.2024 08:53Кстати, я был неправ, что всегда было 6ит. Тут староверы говорят раньше 80бит поддерживалось, но решено было убрать.
Ну тут пример как раз плохого дизайна API. Мало того, что не сделали как все возврат HRESULT с out параметром, так ещё и генерировали это своей утилитой. Ну и наткнулись на неожиданное поведение компилятора. Моглм бы конечно решить, но видимо мало нуждающихся в C API.
Можно попросить, чтобы вернули если очень надо.
Пользуясь случаем , а зачем вам именно C, в не C++?
Serpentine
23.08.2024 08:53Можно попросить, чтобы вернули если очень надо.
Наверное не стоит, т.к. если честно, то использовать С с Direct2D или такими сложными API как DirectX 11 или 12 даже с поддержкой производителя - то еще извращение. Если с OpenGL такое прокатит, то вот с ними - нет. Вся математика у них с перегруженными операторами.
Пользуясь случаем , а зачем вам именно C, в не C++?
Как я говорил, я не профессиональный программист. Прихожу с работы уставший, и если на обычном С с воодушевлением пишу, то при переходе на C++ меня почему-то начинает клонить в сон, писать долго, многое не знаю как написать. Обычно даже C++ туториалы переписываю на простой С. Из последних переписал простой 3D шутер. Было интересно.
NN1
23.08.2024 08:53На правах шутки: https://www.instagram.com/reel/C7y93VkoXuO/
Если уже есть желание научиться чему-то новому, я бы посоветовал тогда взять переписывание на Rust .
При чём можно писать даже в стиле C для начала. Ничего зазорного в этом нет, не нужно слушать догматиков.
eptr
23.08.2024 08:53+2Значит, было UB, теперь ещё добавили очень сомнительное EB.
Видимо, впервые поставив вопрос быстродействия не на первое место.Как-то Саттер обходит стороной вопрос, что должно быть, если валидное значение совпадает с pattern'ом, который генерируется компилятором для "ошибочных" значений:
Мелким шрифтом: компиляторы C++26 обязаны заставить строку 4 переписать биты известным значением, и мотивированы сообщить о проблеме в строке 5 (но не обязаны этого делать).
Как они могут сообщить о проблеме, если все возможные значения — валидны?
И ещё вопрос совместимости с предыдущим кодом как-то тоже упущен.
Ничего,
export template
тоже пришлось потом убрать из следующих версий стандарта, как и спецификациюthrow
для функций.aamonster
23.08.2024 08:53Как они могут сообщить о проблеме, если все возможные значения — валидны?
Речь же идёт о компиляторе, а не о проверке в рантайме. Так что по факту отсутствия присваивания этой переменной, если такой факт удастся доказать (а вот в C# наоборот: если не удалось доказать, что переменная присваивается – ошибка).
eptr
23.08.2024 08:53+2Речь же идёт о компиляторе, а не о проверке в рантайме.
// В C++26 для локальных переменных // Применение фундаментального типа наподобие 'int' int a; // декларация с неким ошибочным значением std::cout << a; // выводит ? или прекращает выполнение: чтение ошибочного значения
В комментарии к 5-ой строке написано: "выводит ? или прекращает выполнение: чтение ошибочного значения".
Очевидно, что имеется ввиду runtime.
aamonster
23.08.2024 08:53+1Опаньки. Что-то я невнимательно прочитал (что Герб Сатор неправильно пересказал стандарт – всё-таки маловероятно; на всякий случай надо глянуть оригинал и стандарт).
Это уже требуется расширение типа – из int в Optional<int>. И соптимизирует ли это компилятор – бог весть (и там, где соптимизирует – лучше бы не ждать рантайма, а выдать результат при компиляции). Выглядит, как отказ от принципа zero overhead.
y_mur
23.08.2024 08:53+4Я тоже "старый ворчун", который любит С++ именно за приоритет в производительности. На мой взгляд, в программах 99% кода работает с данными в которых нет никаких secret-тов и по умолчанию лишать этот код производительности ради 1% специального кода работающего с паролями и т.п. на мой взгляд явный перебор. Тем более что и сейчас ничего не мешает писать этот 1% специального кода безопасно. Да, для этого нужно больше внимания и понимания, но как раз этот 1% его в любом случае требует.
Опциональный ключ-ftrivial-auto-var-init=pattern
прекрасен для отладочных сборок и для того самого 1% критичного к безопасности кода, но это не повод пихать его везде не глядя.
Panzerschrek
23.08.2024 08:53На самом деле начинание хорошее - не вызывать
КтулхуUB из-за неинициализированной int переменной. Но лучше всё-же включить все какие можно предупреждения компилятора и желательно также статического анализатора, чтобы отловить большую часть таких случаев.
dv0ich
23.08.2024 08:53Поэтому лучше писать не
int a;
а
int a {};
Первый вариант, как я понимаю, это C-style, который в плюсах вообще нежелателен. На плюсах лучше писать как на плюсах, а не как на си с плюсами.
DrMefistO
23.08.2024 08:53+1Какое же это всё словоблудие: присвоено, инициализировано...
По факту, у тебя просто происходит запись в регистр, который затем задействуется в тех или иных операциях. Присвоено оно или инициализировано - ваще плевать.
Программист С/С++ должен держать в уме ориентировочную низкоуровневую часть языка, как чего используется, и тогда не будем проблем с тем, что у тебя используется неинициализированная переменная/поле и т.п.
Грустно смотреть на эти все обрастания плюсов синтаксическим
сахаромговном с каждым новым стандартом...qw1
23.08.2024 08:53Это всё важно авторам компиляторов, чтобы объяснять свои оптимизации.
Сейчас неинициализированная переменная - UB, и компилятор имеет право полностью вырезать весь код, использующий неопределённое значение.
https://godbolt.org/z/adTYzzEf4DrMefistO
23.08.2024 08:53Я об этом и говорю: инициализируй там же, где объявляешь, и не будет проблем.
y_mur
23.08.2024 08:53+2Ага, особенно когда выделяешь память под массив, который по ссылке передается функции, которая его потом заполнит данными ;)
qw1
23.08.2024 08:53Я об этом и говорю
Вы говорите, что делать программистам. А стандарт решает проблемы компиляторописателей - им приходится работать с тем кодом, который им дали, и повлиять на него они не могут.
DrMefistO
23.08.2024 08:53+2Т.е. проблема в том, что программист кода должен бороться с поведением (программиста) компилятора вместо того, чтобы заниматься непосредственно кодом. Ок.
qw1
23.08.2024 08:53Мне тоже не нравится оторванность писателей Стандарта от проблем прикладных программистов. Я лишь пытаюсь, почему так получается, и у меня вырисовывается объяснение, что разработчики компиляторов просто ближе к Комитету и потому их мнение перевешивает.
aamonster
23.08.2024 08:53Почему обязательно вырезать? Имеет право поставить на его место код форматирования винчестера: https://habr.com/ru/companies/infopulse/articles/338812/
Dooez
23.08.2024 08:53Переменная на стеке совсем не факт что в регистре находится. При "выделении" переменой на стеке может вообще ничего не происходить, а просто увеличится указатель стека, причем сразу на размер нескольких переменных.
Разница между присваиванием и инициализацией для int может и не существенная, но эти термины работают и для сложных типов. А если писать дженерик код, то уже нельзя заранее знать будет ли разница, и стоит предполагать что будет. Это не синтаксический сахар, это основа системы классов C++.
donlocura
23.08.2024 08:53Тлдр: чтобы детишки больше не могли ударить себя граблями по лбу, в новом стандарте все грабли будут разобраны на части.
Собсна, мой главный вопрос - в коде
int a; int * a_ptr = &a; printf("%d", *a_ptr);
будет какое поведение?
Если возникнет ошибка или выведется заготовленное плохое значение, значит компилятор собрал программу, которая потратила ресурсы на инициализацию переменной, о чем разраб не просил.
Иначе - это новое ограничение языка, выдающееся как фича нового стандарта, работает лишь в некоторых случаях и обходится одной дополнительной строкой.
qw1
23.08.2024 08:53+1будет какое поведение?
Будет выведено заранее известное, детерминированное число, выбранное компилятором для заполнения неинициализированных переменных. Например, как тут предлагали, 0xAAAAAAAA. Или 0xCCCCCCCC, как сейчас делает MSVC с ключом /RTCs
значит компилятор собрал программу, которая потратила ресурсы на инициализацию переменной, о чем разраб не просил
В большинстве случаев чтение неинициализированной переменной это ошибка. Случаев, где компилятор сгенерирует код, который его не просили, должно быть пренебрежимо мало, и они будут лишь улучшать стабильность работы программы, закрывать уязвимости типа чтения секретов.
vadimr
23.08.2024 08:53Если компилятор будет подставлять волшебное значение только для тех переменных, которые, как ему известно, не инициализированы, то не лучше ли просто выдать ошибку? Я думаю, что авторы нововведения имели в виду не это, а то, что все переменные будут инициализироваться волшебным значением, если только компилятору не видно, что они сразу переприсваиваются (как в обычном удалении избыточных присваиваний при оптимизации). А это совсем другое дело с точки зрения эффективности.
Представьте себе огромный разреженный массив, который сейчас в системах с виртуальной памятью почти ничего не стоит.
qw1
23.08.2024 08:53не лучше ли просто выдать ошибку?
Не вариант, потому что куча кода перестанет компилироваться (в том числе такого, который 10 лет заброшен и чинить его никто не захочет).
все переменные будут инициализироваться волшебным значением, если только компилятору не видно, что они сразу переприсваиваются (как в обычном удалении избыточных присваиваний при оптимизации)
Я тоже так думаю. Первый шаг компилятора - инициализировать всё, а далее оптимизатор вырезает избыточные присваивания.
Представьте себе огромный разреженный массив
На стеке слабо представляю. Даже если я пишу
int main() { char buf[0x400000]; }
компилятор для Windows сгенерит цикл, который последовательно потрогает страницы стека с шагом 0x1000, т.к. стек выделяется по мере использования и прыгнуть сразу на 100K вниз запрещено - память не замапится. В Linux это как-то по-другому работает и компилятор не генерирует такой цикл.
Но да, для "средних" буферов, например 2-3 кб, компилятору будет практически невозможно доказать, какие области будут читаться и писаться в фунции, и поэтому он вынужден будет делать ненужный memset.
vadimr
23.08.2024 08:53В Линуксе виртуальное адресное пространство для стека резервируется на этапе линковки, а программа просто использует сегменты указанного в исполняемом файле размера.
qw1
23.08.2024 08:53Программа на этапе линковки не знает, сколько я запущу потоков. А под каждый поток нужен отдельный стек. В Windows есть механизм Guard Page, при попадании в которую происходит исключение и выделяется ещё одна страница.
Вряд ли в линуксе выделяется сразу весь зарезервированный под поток стек. Скорее, промахиваться можно больше чем на одну страницу, и ОС выделит много страниц сразу до места сбоя. В любом случае, "разреженный массив" на стеке я думаю не заработает как планировалось.
vadimr
23.08.2024 08:53В многопоточном коде второй и далее стек отдельно создаётся при создании потока.
А так я в линуксе, бывало, указывал двухгигабайтный стек и не парился.
Dooez
23.08.2024 08:53Представьте себе огромный разреженный массив, который сейчас в системах с виртуальной памятью почти ничего не стоит.
Специально для таких случаев будет атрибут
[[indeterminate]]
.qw1
23.08.2024 08:53Не вариант, потому что много легаси-кода в стиле
char buf[0x10000]; int sz = fread(buf, 1, sizeof(buf), infile); fwrite(buf, 1, sz, outfile);
И получается, впустую перезаписываем 64к, потому что никто в этих местах не будет расставлять атрибуты.
Dooez
23.08.2024 08:53Да, переход на новый стандарт может быть проблемным. Но это не повод оставлять UB навсегда. Без тестов сложно сказать наверняка, но в большинстве случаев IO будет значительно медленнее обнуления памяти. В критически важных местах необходимо будет вручную добавить атрибуты. Кроме того компиляторы могут добавить специальные правила для стандартных функций и не проводить инициализацию.
Предлагаемый подход позволит легаси коду работать, а компиляторы будут заботиться о скорости.
S_WW
23.08.2024 08:53Бессмысленно говорить о том, в какой строке будет выполнена инициализация. После компиляции никаких строк нет. Стековые переменный создаются просто изменением указателя стека. Для экономии времени компилятор может сделать это один раз чтобы создать сразу все переменные для данной функции. А если, например, компилятор разместит переменную в регистре? Просто с какого-то момента будет считаться, что в данный регистр содержит эту переменную.
vadimr
23.08.2024 08:53+1Вкратце говоря, семантика бывает операционная и денотационная. То, что Вы написали, относится к операционной семантике языка C++. Но вопрос о том, в какой строке будет выполнена инициализация, не бессмысленный, а относится к денотационной семантике. То есть к тому, что, собственно, формально означают конструкции языка, в отрыве от того, как они будут реализовываться.
vadimr
Допустим, программист читает неинициализированную переменную по ошибке. Тогда присвоение какого-то начального значения никак не сделает его код правильным.
Допустим, программист читает её умышленно (например, пишет программу-шпион). Тогда что ему делать?
Ну и наконец, что будет, если я объявлю массив на терабайт? Он тоже будет расписываться этой шнягой?
Или это всё действует только в отношении одиночных элементарных значений, но не массивов и не разыменованных указателей (ссылок)? Тогда какой вообще смысл?
artptr86
В 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
, который покажет обращения к неициализированным переменным, то непонятно, кто тогда вообще будет её прогонять в отладчике.vadimr
Ну понятно, что в качестве специальной опции компилятора это осмысленное средство. Но как стандартная семантика языка – выглядит странно.
artptr86
Да, тоже не очень понял. По логике, раз это erroneous behavior, компилятор должен давать ошибку компиляции. Если же вместо этого подобный код будет молча инициализироваться какой-то фигнёй, это выглядит ещё большим вредительством.
vadimr
Всё это имело бы смысл, если б там оказывался какой-нибудь signaling_nan. Но в int его не сделать, не сломав вообще всё в C++.
yrHeTaTeJlb
Стандарт теперь гарантирует, что чтение неинициализированной переменной вернет мусор, а не стригеррит system("rm -rf /"). Теперь поведение стало интуитивно понятным, и это хорошо
qw1
К сожалению, не мусор, а значение, предоставленное компилятором.
Dooez
Если речь про обращение к локальным переменным, то вполне вероятно что компиляторы будут выдавать предупреждение. Если переменная пересекает границу вызова функции по указателю или по ссылке, то компилятор не может знать будет ли чтение из этой переменной.
vanxant
Как раз 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 именно так обычно и делают.geher
На любых архитектурах? Просто я помню, любимый сишный NULL, который большинство программистов полагали заведомо невалидным и равным нулю, в то время как на некоторых архитектураз это был вполне корректный и доступный указатель. Да и в традиционной интеловской архитектуре это вполне может быть доступный адрес прерывания
Абсурдно большое - это только для конкретных применений. В реальности может оказаться вполне адекватным числом. Для char или short int это вообще не вопрос, int в некоторых архитектурах может быть 16 бит, а доя unsigned long int это может оказаться обычная битовая маска.
Лучше было бы, чтобы компилятор грязно ругался на использование неинициализированной переменной. Впрочем, оно и так уже за это предупреждения раздает.
me21
Мне почему-то кажется, что в дебаг сборках компиляторы уже давно так делают. Хотя, может быть, я это видел только в компиляторе Microsoft Visual Studio, не помню точно.
yatanai
Статья эта какой-то мусор и не объясняет почему оно так работает.
Итак.... Любое чтение тривиального типа который не содержит значения является UB. Значение же приобретается либо в конструкторе(не дефолтном), либо в присвоением уже инициализированного объекта. Если тип это агрегат, в том плане что просто хранит другие тривиальные типы, то если он не до конца инициализирован, то обращение к этим не инициализированым полям или присвоение этого объекта другому тоже UB. И да, все встроенные/фундаментальные типы являются тривиальными по умолчанию.
Если до С++20, то копированием сырых байт в уже подготовленный объект. Если после С++20, то использовать bit_cast который по сути копирует уже подготовленные типы, если после С++23, то start_lifetime_as который не копирует а сразу представляет память как объект. В любых других случая молится господу что ваш конкретный компилятор сможет сгенерировать нужный вам код, или попытаться покалдовать с ассемблером или несовершенством линковки TU.
ЗЫ этот язык буквально ментальное оружие. Либо ты освоишь его и сможешь кабанов с одного выстрела убивать, либо тебе оторвёт ноги и ты будешь жаловаться что оно кривое и ваще я капитаном корабля хотел стать)