Недавно я получил по почте от Сэма Джонсона этот вопрос. Вот слегка отредактированное письмо Сэма:

«Возьмём для примера этот код в локальной области видимости функции:

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++, сделав его максимально убедительным. Возможно, комитету оно понравится, или же он найдёт незамеченные мной недостатки... Посмотрим! Как бы то ни было, я буду сообщать о новостях в своём блоге.

Ещё раз благодарю Сэма Джонсона за этот вопрос!

Комментарии (61)


  1. vadimr
    23.08.2024 08:53
    +6

    Допустим, программист читает неинициализированную переменную по ошибке. Тогда присвоение какого-то начального значения никак не сделает его код правильным.

    Допустим, программист читает её умышленно (например, пишет программу-шпион). Тогда что ему делать?

    Ну и наконец, что будет, если я объявлю массив на терабайт? Он тоже будет расписываться этой шнягой?

    Или это всё действует только в отношении одиночных элементарных значений, но не массивов и не разыменованных указателей (ссылок)? Тогда какой вообще смысл?


    1. artptr86
      23.08.2024 08:53
      +1

      Допустим, программист читает её умышленно (например, пишет программу-шпион). Тогда что ему делать?

      В 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, который покажет обращения к неициализированным переменным, то непонятно, кто тогда вообще будет её прогонять в отладчике.


      1. vadimr
        23.08.2024 08:53
        +2

        Ну понятно, что в качестве специальной опции компилятора это осмысленное средство. Но как стандартная семантика языка – выглядит странно.


        1. artptr86
          23.08.2024 08:53
          +4

          Да, тоже не очень понял. По логике, раз это erroneous behavior, компилятор должен давать ошибку компиляции. Если же вместо этого подобный код будет молча инициализироваться какой-то фигнёй, это выглядит ещё большим вредительством.


          1. vadimr
            23.08.2024 08:53

            Всё это имело бы смысл, если б там оказывался какой-нибудь signaling_nan. Но в int его не сделать, не сломав вообще всё в C++.


          1. yrHeTaTeJlb
            23.08.2024 08:53

            Стандарт теперь гарантирует, что чтение неинициализированной переменной вернет мусор, а не стригеррит system("rm -rf /"). Теперь поведение стало интуитивно понятным, и это хорошо


            1. qw1
              23.08.2024 08:53

              К сожалению, не мусор, а значение, предоставленное компилятором.


      1. vanxant
        23.08.2024 08:53
        +2

        Как раз 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 именно так обычно и делают.


        1. geher
          23.08.2024 08:53

          На 64-битных машинах это заведомо невалидный указатель

          На любых архитектурах? Просто я помню, любимый сишный NULL, который большинство программистов полагали заведомо невалидным и равным нулю, в то время как на некоторых архитектураз это был вполне корректный и доступный указатель. Да и в традиционной интеловской архитектуре это вполне может быть доступный адрес прерывания

          это будет абсурдно большое число.

          Абсурдно большое - это только для конкретных применений. В реальности может оказаться вполне адекватным числом. Для char или short int это вообще не вопрос, int в некоторых архитектурах может быть 16 бит, а доя unsigned long int это может оказаться обычная битовая маска.

          Это намного лучше, чем, допустим, 0. Если вместо нуля ожидалось какое-то малое слагаемое, его отсутствие можно и не заметить.

          Лучше было бы, чтобы компилятор грязно ругался на использование неинициализированной переменной. Впрочем, оно и так уже за это предупреждения раздает.


      1. me21
        23.08.2024 08:53
        +1

        Мне почему-то кажется, что в дебаг сборках компиляторы уже давно так делают. Хотя, может быть, я это видел только в компиляторе Microsoft Visual Studio, не помню точно.


    1. yatanai
      23.08.2024 08:53

      Статья эта какой-то мусор и не объясняет почему оно так работает.

      Итак.... Любое чтение тривиального типа который не содержит значения является UB. Значение же приобретается либо в конструкторе(не дефолтном), либо в присвоением уже инициализированного объекта. Если тип это агрегат, в том плане что просто хранит другие тривиальные типы, то если он не до конца инициализирован, то обращение к этим не инициализированым полям или присвоение этого объекта другому тоже UB. И да, все встроенные/фундаментальные типы являются тривиальными по умолчанию.

      Допустим, программист читает её умышленно (например, пишет программу-шпион). Тогда что ему делать?

      Если до С++20, то копированием сырых байт в уже подготовленный объект. Если после С++20, то использовать bit_cast который по сути копирует уже подготовленные типы, если после С++23, то start_lifetime_as который не копирует а сразу представляет память как объект. В любых других случая молится господу что ваш конкретный компилятор сможет сгенерировать нужный вам код, или попытаться покалдовать с ассемблером или несовершенством линковки TU.

      ЗЫ этот язык как ментальное оружие. Либо ты освоишь его и сможешь кабанов с одного выстрела убивать, либо тебе оторвёт ноги и ты будешь жаловаться что оно кривое и ваще я капитаном корабля хотел стать)


  1. vilgeforce
    23.08.2024 08:53

    Надо посмотреть во что оно скомпилится. Может, переменной вовсе не будет...


  1. viordash
    23.08.2024 08:53
    +4

    имхо, достаточно просто падать с ошибкой, при компиляции таких переменных. И пусть программист сам решает чем проиницилизировать. Не всегда дефолтное значение это синоним безопасного поведения.


    1. yrHeTaTeJlb
      23.08.2024 08:53
      +1

      Это сломает кучу легаси


      1. viordash
        23.08.2024 08:53

        или поможет починиться:)


  1. ExternalWayfarer
    23.08.2024 08:53
    +7

    Недавно я получил по почте от Сэма Джонсона этот вопрос.

    Кто это? Мы должны его знать?


    1. aamonster
      23.08.2024 08:53

      Нет, достаточно знать Герба Саттера.


  1. aamonster
    23.08.2024 08:53
    +4

    Довольно странное решение. Я бы понял решение в духе C# – когда на всех путях переменная до чтения гарантированно должна быть присвоена, иначе программа не скомпилируется, но присваисвание ей какого-то неизвестного значения (0xDEADBEEF, что ли?) не выглядит серьёзным улучшением по сравнению с мусором из стека.


    1. KanuTaH
      23.08.2024 08:53
      +3

      Я бы понял решение в духе C# – когда на всех путях переменная до чтения гарантированно должна быть присвоена, иначе программа не скомпилируется

      Это нереально. Простой пример:

      // Модуль A
      
      void foo(int&);
      
      // Модуль B
      
      int main()
      {
        int a;
        foo(a);
        ++a;
      }

      Предположим что модуль A распространяется в виде уже скомпилированной библиотеки (DLL или .so). Откуда компилятор знает, что делает функция foo()? Инициализирует ли она переменную a? А может быть наоборот, она из нее только читает? Он не может быть в этом уверен. Если в подобных случаях заставлять погромиста явно инициализировать a до вызова foo(), то нужно будет править огромное количество существующего кода, который совершенно нормально работает, чисто для того, чтобы удовлетворить компилятор. Никто на это не пойдет.

      но присваисвание ей какого-то неизвестного значения (0xDEADBEEF, что ли?) не выглядит серьёзным улучшением по сравнению с мусором из стека.

      Дело не в "мусоре из стека", а в том, что сейчас компилятор считает, что чтение неинициализированной переменной просто не может происходить. И делает разнообразные оптимизации (например, выкидывание "лишнего" по его мнению кода) исходя из этого. После введения erroneous behavior компилятор уже будет в курсе, что такое чтение происходить может, просто это ненормально. И будет заниматься преобразованием и оптимизацией кода исходя уже из новых представлений.


      1. aamonster
        23.08.2024 08:53

        Да я знаю про оптимизации. И понятно, что компилятор теряет возможность проводить оптимизации, полагаясь на то, что программист не будет читать переменную до записи в неё. Т.е. возможный выигрыш в быстродействии потеряли, а от ошибок не избавились.


        1. KanuTaH
          23.08.2024 08:53
          +1

          От ошибок в том виде, как это сделано в C# (на стадии компиляции), тут и невозможно избавиться. Можно лишь облегчить их последствия.


      1. vadimr
        23.08.2024 08:53

        Конкретно в приведённом листинге проблема только в том, что в синтаксисе C++ не указывается направление передачи параметров.

        В каком-нибудь Фортране, если параметр описан как intent(in) или intent(inout), то это некорректный код, а если как intent(out) – то корректный. И компилятор, соответственно, это сечёт.

        Лучше б в C++ добавили intent.


        1. KanuTaH
          23.08.2024 08:53

          Лучше б в C++ добавили intent.

          Даже если предположить, что кто-то решит сделать нечто подобное, то, во-первых, это ничего не гарантирует в плане инициализации, потому что нет никаких гарантий, что в этот intent(out) в конце концов функцией foo() будет хоть что-то записано. Она может выйти и не трогать этот параметр вообще. А во-вторых, со старым кодом это все работать не будет. В общем, этот гипотетический intent не решает ни одной реальной проблемы, просто очередные шашечки.


          1. vadimr
            23.08.2024 08:53

            Если в функции foo будет путь, в котором выходной параметр не присваивается, компилятор выдаст предупреждение на код этой функции. Точно так же, как выдаёт на функцию, не возвращающую результат.

            Со старым кодом, конечно, это всё не работает (и в Фортране тоже), но в целом это полезный механизм.


            1. KanuTaH
              23.08.2024 08:53
              +1

              Если в функции foo будет путь, в котором выходной параметр не присваивается, компилятор выдаст предупреждение на код этой функции.

              В очень многих C-like API сделано так, что функция не пишет ничего в свой "выходной" параметр в случае, если произошла ошибка. К примеру, в том же POSIX read() не обязан инициализировать целевой буфер при возникновении ошибки. Думаю, если начать намеренно ломать такие вещи, многие будут недовольны.


          1. NN1
            23.08.2024 08:53

            Как раз проконтролировать запись при intent(out) это возможно , аннотации кода уже умеют это делать.

            А вот то что старый код останется , это так.


      1. VBDUnit
        23.08.2024 08:53
        +1

        В 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);

        даже если это не имеет смысла. Это уже пусть компилятор разбирается и оптимизирует.


        1. qw1
          23.08.2024 08:53
          +1

          void* buffer = malloc(512);
          fread(out buffer, 2, 256, file);
          

          Так он будет требовать перезаписи указателя buffer. А как указать, что надо перезаписать память по 1-му параметру, размером = произведение 2-го и 3-го параметра?


          1. VBDUnit
            23.08.2024 08:53

            Ну можно пофантазировать. Первое что пришло в голову:

            fread(*out buffer, 2, 256, file);
            //не путать с out *buffer

            Вообще у меня стойкое ощущение, что в компиляторы в будущем должен быть встроен ИИ, хотя бы для оптимизаций и варнингов, а может и ещё чего‑нибудь. И не так уж ужасно будет недетерминированное поведение, как его малюют.


            1. KanuTaH
              23.08.2024 08:53

              в компиляторы в будущем должен быть встроен ИИ

              Лучше не надо :)


        1. KanuTaH
          23.08.2024 08:53
          +1

          Соответственно, либо мы тащим эту штуку в c++, либо просто говорим, что больше делать вот так нельзя, можно только вот так даже если это не имеет смысла. Это уже пусть компилятор разбирается и оптимизирует.

          Мде... Спасибо, конечно, за такой великолепный заход, но лучше не надо. Подобные эксперименты лучше ставить на чем-нибудь таком, чем пользуется 3.5 человека в свое свободное время just for fun.


  1. rukhi7
    23.08.2024 08:53
    +12

    Очередная высосанная из пальца проблема. Как мы жили десятки лет не зная где у нас инициализация в двух строчках кода.


    1. yrHeTaTeJlb
      23.08.2024 08:53

      Не жили богато, нечего и начинать

      То что в C++ ub на каждом шагу это не высосанная из пальца проблема. Чем меньше в коде мест, которые могут скомпилироваться в system("rm -rf /"), тем лучше


  1. ForestDront
    23.08.2024 08:53
    +5

    Во наклепали стандартов. Куда вы гоните? Я ещё С++98 не полностью освоил.


    1. Lainhard
      23.08.2024 08:53

      У меня так с прошлой работы люди гитом не хотели пользоваться.


      1. gyzl
        23.08.2024 08:53

        Я им пользуюсь (и успешно) уже 10 лет, но всё ещё НЕ ХОЧУ его использовать :)


        1. qrKot
          23.08.2024 08:53

          для интереса: чем вызвано "нехотение"? Отторжение cvs как таковых, или просто другая cvs'ка была "ближе к телу"?


          1. gyzl
            23.08.2024 08:53

            Избыточность и как следствие, сложность, для 95% повседневных задач. Несмотря на дружелюбные GUI, часто Git становится поперек дороги, и я вынужден его изучать, хотя это мне не нужно, я не собираюсь стать в нём специалистом и написать книгу про Git. Это как если бы бухгалтер, который ездит на работу на автобусе, был вынужден уметь ремонтировать этот автобус.
            Ну и да, синдром утёнка с SVN.


  1. AVKinc
    23.08.2024 08:53

    Есть компиляторы которые всем глобальным переменным присваивает ноль если мы не сделали свою инициализацию.


    1. Serpentine
      23.08.2024 08:53

      Есть компиляторы которые всем глобальным переменным присваивает ноль если мы не сделали свою инициализацию.

      Может вы имеете ввиду "локальным переменным", т.к. неинициализированные глобальные переменные по стандарту и так обнулены.

      А если такой компилятор локальные переменные обнуляет, тогда где они хранятся в процессе работы? Если на стеке, то при каждом вызове функции их обнулять в стековом фрейме очень затратно будет.


    1. aamonster
      23.08.2024 08:53

      Глобальным – по стандарту положено. Тут речь про локальные.


  1. MichaelBorisov
    23.08.2024 08:53
    +2

    К сожалению, ввод новых стандартов 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 лет на нем не будет возможно писать реально портируемый код, который поддерживается основной массой компиляторов.


    1. NN1
      23.08.2024 08:53

      MSVC давно поддерживает C99, и даже C17:

      https://learn.microsoft.com/en-us/cpp/overview/visual-cpp-language-conformance?view=msvc-170


      1. Serpentine
        23.08.2024 08:53
        +1

        Только по этой же ссылке написано, что:

        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.


        1. 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


          1. Serpentine
            23.08.2024 08:53
            +1

            В MSVC Long Double всегда был 64 бит, но это также не является отклонением от стандарта.

            Я это знаю, просто большинство компиляторов в современном мире представляют его как число расширенной точности, а не двойной. И без танцев с бубном.

            И проголосовать

            Там обсуждение трехлетней давности.

            VLA также опционально не реализован в MSVC, некоторые считают что и в GCC не нужно было реализовывать.

            Они много всего не реализовали, как и gcc много всякого реализовал, на первый взгляд, лишнего (это шутка). Лично мне эти приколы с VLA тоже непонятны - ломается преимущество языка - zero runtime.

            В целом с MSVC работать на С гораздо неудобно даже не из-за ограниченной поддержки, а потому что они свои (вроде как даже нативные и написанные на С) API ломают в неожиданное время и в неожиданном месте. Я не профессиональный программист и несколько дней убил, пытаясь запустить свою программу на С с Direct2D и только потом наткнулся на запись в их блоге, что они сишные заголовки удалили из SDK.


            1. NN1
              23.08.2024 08:53

              Кстати, я был неправ, что всегда было 6ит. Тут староверы говорят раньше 80бит поддерживалось, но решено было убрать.

              Ну тут пример как раз плохого дизайна API. Мало того, что не сделали как все возврат HRESULT с out параметром, так ещё и генерировали это своей утилитой. Ну и наткнулись на неожиданное поведение компилятора. Моглм бы конечно решить, но видимо мало нуждающихся в C API.

              Можно попросить, чтобы вернули если очень надо.

              Пользуясь случаем , а зачем вам именно C, в не C++?


              1. Serpentine
                23.08.2024 08:53

                Можно попросить, чтобы вернули если очень надо.

                Наверное не стоит, т.к. если честно, то использовать С с Direct2D или такими сложными API как DirectX 11 или 12 даже с поддержкой производителя - то еще извращение. Если с OpenGL такое прокатит, то вот с ними - нет. Вся математика у них с перегруженными операторами.

                Пользуясь случаем , а зачем вам именно C, в не C++?

                Как я говорил, я не профессиональный программист. Прихожу с работы уставший, и если на обычном С с воодушевлением пишу, то при переходе на C++ меня почему-то начинает клонить в сон, писать долго, многое не знаю как написать. Обычно даже C++ туториалы переписываю на простой С. Из последних переписал простой 3D шутер. Было интересно.


                1. NN1
                  23.08.2024 08:53

                  На правах шутки: https://www.instagram.com/reel/C7y93VkoXuO/

                  Если уже есть желание научиться чему-то новому, я бы посоветовал тогда взять переписывание на Rust .

                  При чём можно писать даже в стиле C для начала. Ничего зазорного в этом нет, не нужно слушать догматиков.

                  https://cliffle.com/p/dangerust/


  1. eptr
    23.08.2024 08:53
    +1

    Значит, было UB, теперь ещё добавили очень сомнительное EB.
    Видимо, впервые поставив вопрос быстродействия не на первое место.

    Как-то Саттер обходит стороной вопрос, что должно быть, если валидное значение совпадает с pattern'ом, который генерируется компилятором для "ошибочных" значений:

    Мелким шрифтом: компиляторы C++26 обязаны заставить строку 4 переписать биты известным значением, и мотивированы сообщить о проблеме в строке 5 (но не обязаны этого делать).

    Как они могут сообщить о проблеме, если все возможные значения — валидны?

    И ещё вопрос совместимости с предыдущим кодом как-то тоже упущен.

    Ничего, export template тоже пришлось потом убрать из следующих версий стандарта, как и спецификацию throw для функций.


    1. aamonster
      23.08.2024 08:53

      Как они могут сообщить о проблеме, если все возможные значения — валидны?

      Речь же идёт о компиляторе, а не о проверке в рантайме. Так что по факту отсутствия присваивания этой переменной, если такой факт удастся доказать (а вот в C# наоборот: если не удалось доказать, что переменная присваивается – ошибка).


  1. y_mur
    23.08.2024 08:53
    +2

    Я тоже "старый ворчун", который любит С++ именно за приоритет в производительности. На мой взгляд, в программах 99% кода работает с данными в которых нет никаких secret-тов и по умолчанию лишать этот код производительности ради 1% специального кода работающего с паролями и т.п. на мой взгляд явный перебор. Тем более что и сейчас ничего не мешает писать этот 1% специального кода безопасно. Да, для этого нужно больше внимания и понимания, но как раз этот 1% его в любом случае требует.
    Опциональный ключ -ftrivial-auto-var-init=pattern прекрасен для отладочных сборок и для того самого 1% критичного к безопасности кода, но это не повод пихать его везде не глядя.


  1. Panzerschrek
    23.08.2024 08:53

    На самом деле начинание хорошее - не вызывать Ктулху UB из-за неинициализированной int переменной. Но лучше всё-же включить все какие можно предупреждения компилятора и желательно также статического анализатора, чтобы отловить большую часть таких случаев.


  1. dv0ich
    23.08.2024 08:53

    Поэтому лучше писать не

    int a;

    а

    int a {};

    Первый вариант, как я понимаю, это C-style, который в плюсах вообще нежелателен. На плюсах лучше писать как на плюсах, а не как на си с плюсами.


  1. DrMefistO
    23.08.2024 08:53

    Какое же это всё словоблудие: присвоено, инициализировано...

    По факту, у тебя просто происходит запись в регистр, который затем задействуется в тех или иных операциях. Присвоено оно или инициализировано - ваще плевать.

    Программист С/С++ должен держать в уме ориентировочную низкоуровневую часть языка, как чего используется, и тогда не будем проблем с тем, что у тебя используется неинициализированная переменная/поле и т.п.

    Грустно смотреть на эти все обрастания плюсов синтаксическим сахаром говном с каждым новым стандартом...


    1. qw1
      23.08.2024 08:53

      Это всё важно авторам компиляторов, чтобы объяснять свои оптимизации.
      Сейчас неинициализированная переменная - UB, и компилятор имеет право полностью вырезать весь код, использующий неопределённое значение.
      https://godbolt.org/z/adTYzzEf4


      1. DrMefistO
        23.08.2024 08:53

        Я об этом и говорю: инициализируй там же, где объявляешь, и не будет проблем.


        1. y_mur
          23.08.2024 08:53

          Ага, особенно когда выделяешь память под массив, который по ссылке передается функции, которая его потом заполнит данными ;)


        1. qw1
          23.08.2024 08:53

          Я об этом и говорю

          Вы говорите, что делать программистам. А стандарт решает проблемы компиляторописателей - им приходится работать с тем кодом, который им дали, и повлиять на него они не могут.


          1. DrMefistO
            23.08.2024 08:53

            Т.е. проблема в том, что программист кода должен бороться с поведением (программиста) компилятора вместо того, чтобы заниматься непосредственно кодом. Ок.


      1. aamonster
        23.08.2024 08:53

        Почему обязательно вырезать? Имеет право поставить на его место код форматирования винчестера: https://habr.com/ru/companies/infopulse/articles/338812/