Pull to refresh

Comments 33

Варианты:

  1. Перевод (тогда не хватает ссылки на оригинал)

  2. ChatGPT

  3. 1 + 2 (перевод с помощью ChatGPT)

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

obj = { 2, 4, 6 };

Правильно, она вызовет этот самый конструктор. Для уже инициализированного объекта… Да ладно, парни! Списки инициализаторов добавляют сумятицы.

Неверно. Она создаст новый временный объект, а потом переместит его в obj путем вызова у него соответствующего operator=(). Дальше читать этот позор не стал. Написано скорее всего чатгпт "на основе" подобных статей (AI очень любит нести полную херню с уверенным видом), ну либо некомпетентным человеком.

Ну не стал и не стал, о чем тогда говорить-то?

А если Вы operator=() не описали, то будет у Вас переписанный obj.

А если Вы operator=() не описали, то будет у Вас переписанный obj.

Если я operator=() не описал, то он будет сгенерирован компилятором - за исключением тех случаев, когда это сделать невозможно, тогда будет ошибка компиляции. Изучите матчасть наконец.

Во-первых, Вы тут никому не KanuTaH и сделайте лицо попроще, пожалуйста.

Во-вторых, ...он будет сгенерирован компилятором... ИМЕННО. И в результате ...будет у Вас переписанный obj...

Справедливости ради, я действительно не учел S& operator=(S&&). Похоже, что часть предлагаемого в статье уже работает. Радоваться этому или нет пока не ясно.

И в результате ...будет у Вас переписанный obj...

Это не то же самое, что повторный вызов конструктора для уже инициализированного объекта, как вы(?) пишете в статье.

Справедливости ради, я действительно не учел S& operator=(S&&). Похоже, что часть предлагаемого в статье уже работает. Радоваться этому или нет пока не ясно.

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

Перевод. Ссылка на оригинал начинается так: C:\Users\ ...

Но каким бы умным Вы не были, Вам его оттуда не достать. :)

И причем здесь ChatGPT?

https://en.cppreference.com/w/cpp/language/attributes/indeterminate

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

От агрегатной инициализации это отличается тем, что мы не обнуляем опущенное 

АУЕ какое-то

Здесь все, что не инициализировано указанными значениями, инициализировано нулем.

Не факт, поскольку не обозначен контекст.

Если это код внутри какой-то функции (например, main), то нулевые значения не гарантированы.

Конечно в С++ с инициализацией наворотили лишнего. Зачем было вводить "универсальную инициализацию," если проблема была в другом - в том что нужно было ввести новый синтаксис функций (и его все равно ведь ввели - с auto и стрелочкой). И вообще добавление новых фич в С++ становится все более фрагментарным и бессистемным.

В целом я согласен - круглые скобки это конструктор (выполнение кода), фигурные с присваиванием - поэлементное присваивание публичных полей (т.е. даже и не инициализация как таковая, а просто присваивание нескольких полей одним оператором). Фигурные скобки без оператора присваивания вообще не стоило вводить. А вот что стоило, так это ввести универсальную концепцию кортежей - как раз тех самых составных литералов в фигурных скобках - как структурно типизированных объектов. Тогда бы можно было писать что-то вроде {i, j, k} = {1,2,3}, не потребовался бы отдельный синтаксис "структурных привязок" и т.д.

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

Это пока вы не пишете обобщенный код типа такого:

template<typename T, typename... Ts>
auto foo(Ts&&... args)
{
  T obj(std::forward<Ts>(args)...);
  
  // Do something with obj...
  
  return obj;
}

Сейчас этот код может работать и с агрегатами, и с классами, имеющими конструкторы, принимающие аргументы, и с классами, располагающими только конструктором по умолчанию (без аргументов). А теперь представьте, что для инициализации каждого из этих вариантов нужен был бы отдельный синтаксис... Нужно было бы заниматься совершенно лишними приседаниями.

Ох уж эти шаблоны:) Но я не против унифицированной инициализации всего в круглых скобках (да и вообще я всегда за универсальность во всем). А зачем добавили фигурные? (на Хабре кстати множество статей с подробным разбором инициализации, например эта). Для решения проблемы "vexing parse" и для инициализации контейнеров. Первое - это проблема кривого синтаксиса функций (функция должна начинаться с ключевого слова, как во всех современных языках, а не с возвращаемого типа). Второе решается введением концепции "кортежей". Но вместо прямого решения проблемы )пусть и ломающего обратную совместимость) решили поставить заплатку на заплатке и породили еще один синтаксис инициализации.

Ох уж эти шаблоны:)

Шаблоны - это наше все.

Но я не против унифицированной инициализации всего в круглых скобках (да и вообще я всегда за универсальность во всем)

Так и нужно двигаться в этом направлении, как это к слову сделано в C++20, унифицировавшем инициализацию агрегатов, которую теперь можно выполнять как с помощью круглых, так и фигурных скобок (с разницей в обработке narrowing conversions). А идеи типа "давайте выполнять инициализацию агрегатов через присваивание, инициализацию путем вызова конструктора через круглые скобки, а value-initialization как default-initialization, прибьем все это гвоздями, а все остальное задепрекейтим" - это ну, такое. Такого не будет.

функция должна начинаться с ключевого слова, как во всех современных языках

Типа Паскаля? :)

пусть и ломающего обратную совместимость

Да-да, в этом все дело. Для любителей ломать обратную совместимость есть "современные языки".

Типа Паскаля? :)

да, а также типа Go, Rust, Swift, Scala, Kotlin и т.д. :)

Для любителей ломать обратную совместимость есть "современные языки".

Вот здесь я предложил рабочий способ одновременно сломать и сохранить обратную совмесимость. Просто вводим новую версию языка, несовместимую со старой на уровне исходников, но при этом требуем от компиляторов поддерживать обе версии одновременно в рамках проекта - за счет этого получается гарантированная совмесимость на уровне ABI без стандартизации самого ABI. Старую версию замораживаем, все улучшения вносим только в новую, и так постепенно лет за 20 все кому нужно перейдут на новую версию.

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

То, что описано здесь - бесполезно. Из-за поддержки обратной совместимости из языка ничего не удаляется. То есть, если что-то попало в язык, то оно там останется навсегда. Удаление «ненужного» синтаксиса настолько редко, что можном утвердительно написать, что этого нет вовсе. Это не только в C++ так. Странно, что вы этого не знаете, и делаете такие предложения.

Вообще вся проблема инициализации связана с обратной совместимостью. Когда появилась семантика перемещения, появилась проблема понимания, как сделать так, чтобы поведение кода осталось таким же как раньше, но при этом программист мог использовать нововведения. Люди в комитете не дураки. Но они предвзяты. Они хорошо понимают, что слом обратной совместимости потребует переписывание сотни миллионов строк кода. При чём им самим придётся переписывать некоторый legacy спагетийный говно код, который был написан ими же в стародавние времена.

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

В каких-то местах вы правы, что комитет сделал ошибку. Но, увы, всё в угоду обратной совместимости. Есть мнение, что если бы в стандарте C++11 сломали ABI, то язык C++ не был бы столь популярен, как сейчас. Так как переписывать код на новый стандарт не было смысла из-за маячивший на горизонте всепоглощающей Java, а в месте с ней и C#. Там ещё Go стал вырисовываться. И Rust к середине 10-х годов

Заменил "упразднить" на "игнорировать". Строго говоря, первое, имхо, включает в себя и второе, но если это вызывает вопросы, то пусть будет так.

Проблема у вас в том, что вы предлагаете переделать то, что переделывать в языке не будут. Программистов на C++ годами тренировали писать, как пишут они сейчас. И просто так взять и поменять семантику объявления и инициализации никто не даст. Потому что это помимо большой кодовой базы, ещё и проблема обучения молодого поколения. Грубо описывая вы залезли туда, где менять что-то уже нет смысла. Используйте инициализацию через фигурные скобки везде. Это на самом деле проще, чем кажется на первый взгляд. И даже можно, применить ваш же подход, игнорировать те виды инициализации, которые вам не нравятся.

Имейте в виду, что я не консерватор. Я за развитие и модернизацию языка C++. Я хорошо понимаю, что язык C++ сейчас устарел. Он слишком перегружен специфичным поведением, оставшимся от предыдущих поколений программистов. Я сам, как разработчик на C++, жду, что C++2 от Херба Саттера станет новым стандартом, или Carbon от Google заменит этот архаичный ад интерфейсов, или мифический Circle, таки добавит безопасности like in Rust. Но, увы, пока нам придётся жить с тем, что есть, так как комитет, если бы захотел уже давно привёл бы в порядок язык. А раз этого не сделано до сих пор, значит кому-то это нужно

Проблема у вас в том, что вы предлагаете переделать то, что переделывать в языке не будут.

Это у НАС проблема. :)

И просто так взять и поменять семантику объявления и инициализации никто не даст.

Синтаксис не меняем вообще. А там, где в дело идет специальный оператор присвоения (отрывающий фигурные скобки от непосредственно инициализации), нас страхует то самое ключевое слово.

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

:)

Это у НАС проблема. :)

У C++ программистов нет проблем с инициализациями. Потому что используются тот вид инициализации, которым привыкли работать. Остальное просто не используется. Это плохо, но так исторически сложилось. Таков язык C++. А если у вас возникла потребность что-то поменять, то это конкретно лично у вас проблема. Ваша статья выглядит так, будто вы пытаетесь натянуть эту проблему на всех. Представьте, что комитет стоял перед такой же проблемой до 2011 года. И они вышли из ситуации таким образом, какой мы, все программисты на C++, имеем на данный момент. И рыбку съели и сковородку не помыли

Синтаксис не меняем вообще

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

У C++ программистов нет проблем с инициализациями. Потому что используются тот вид инициализации, которым привыкли работать. Остальное просто не используется. Это плохо, но так исторически сложилось. Таков язык C++. А если у вас возникла потребность что-то поменять, то это конкретно лично у вас проблема. Ваша статья выглядит так, будто вы пытаетесь натянуть эту проблему на всех.

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

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

Абзац, где "Через какое-то время это ключевое слово можно было бы упразднить".

Вопросы риторические

Ну почему же? Разумеется, я всего не предусмотрел. Сказать так было бы просто глупостью. Это все - чистая теория. При попытке (чисто гипотетической) реализовать подобное, наверняка вылезет куча нюансов, предусмотреть которые сейчас практически невозможно. Тем не менее, мне это кажется вполне реализуемым и это как раз-таки ничто иное, как попытка разгрузить перегруженный язык.

В любом случае, спасибо за Ваше мнение и комментарии!

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

А почему не будем? Думаете другие не хотели бы посмотреть на материал с которого вы приняли решение написать статью в Хабр? Обычно как раз ссылки это что даёт возможность более глубоко погрузиться в контекст. А в научных кругах ссылки на другие работы показывают, что вы не с потолка взяли первичные вводные данные. Я уже выше писал, что у вас статья натягивание проблемы. Которая как бы существует. Но в текущих реалиях языка C++ несущественна.

Абзац, где "Через какое-то время это ключевое слово можно было бы упразднить".

У вас там нет чёткого различия в какой момент будет вызван конструктор копирования, а в какой конструктор перемещение; в какой оператор присваивания, а в какой оператор перемещения. Как компилятор поймёт, что ему делать? То есть ваша сущность отдельного ключевого слова для операции инициализации это ещё одна функция к уже имеющимся. Это не упрощение и не разгрузка. Это перегрузка. Накладывание поверх существующий логики. Примите как данность, что слома обратной совместимости не будет. Комитет это просто не примет. Пока вы не сможете уместить вашу сущность в логику существующих механизмов инициализаций. Я не удивлюсь тому, что не вы были первым, кто предложил похожую концепцию. Атрибуты в синтаксисе существуют уже давно. Добавить что-то такое не проблема. Проблема не сломать ничего

Это перегрузка.

Это не просто перегрузка. Решил таки пачетать сие творение, и тут все настолько плохо и сумбурно, что даже не знаешь, с чего начать.

Вот, например, автор пишет:

Последний вариант использования конструктора преобразования является потенциально опасным и это, собственно, инициализация объекта:

MyType1 obj1;

MyType2 obj2(obj1); // constructor syntax
MyType2 obj2 = obj1; // assignment syntax

С точки зрения унифицированной инициализации две последние строки идентичны и вызывают конструктор преобразования. А вот в случае трехстадийной инициализации, последняя строка - это вызов конструктора по умолчанию, затем оператора присвоения и, в зависимости от того, что он принимает, возможно еще и конструктора преобразования.

Что тут является таким "потенциально апасным", я даже хз, здесь забавно другое. Получается, что если я захочу иногда делать то, что здесь называется "constructor syntax" (а в стандарте это называется direct-initialization), а иногда - то, что здесь называется "assignment syntax" (а в стандарте - copy-initialization), то мне придется писать И конструктор, И operator=(), так? Потому что про то, что происходит в предпоследней строке в случае, если обычного конструктора с параметром типа MyType1 нет, тут не написано ничего. Отмечу, что сейчас для обоих случаев мне достаточно написать только один не-explicit конструктор. А учитывая, что подавляющее большинство контейнеров, имеющих методы наподобие emplace(), используют именно direct-initialization для создания элементов внутри себя (ведь placement new не умеет в copy-initialization), то...

Далее, вопрос copy elision. Не знаю, знает ли об этом автор (подозреваю, что нет), но сейчас, когда погромист пишет что-нибудь вроде:

SomeType foo()
{
  SomeType obj = {...};

  obj.x = ...;
  [...]
  obj.y = ...;

  return obj;
}

SomeType obj = foo();

компилятор имеет право (а в определенных случаях даже обязан) вызывать конструктор SomeType только один раз. По автору же тут получается чистый "assignment syntax", так вот любопытно, как автор себе это представляет, особенно если у SomeType есть только конструктор по умолчанию и парочка operator=(), ведь этого (по автору) достаточно для его так называемой "трехстадийной инициализации". Из этого не слепишь единственный вызов конструктора ну никак! Компилятор будет вынужден дважды вызывать конструктор по умолчанию, и опять-таки дважды operator=() (по одному каждого типа).

Далее, вопрос эффективности этой самой "трехстадийной инициализации". Сейчас, даже в том теоретическом случае, если компилятор будет всегда заниматься value initialization полей, отсутствующих в списке инициализации конструктора, он всегда сможет сделать это эффективно, ведь компилятор видит, какие поля присутствуют в списке инициализации конструктора, а какие - отсутствуют, и может применить value initialization только к тем из них, которые в списке отсутствуют. В случае же "трехстадийной инициализации" (с вызовом конструктора по умолчанию и operator=() по отдельности) компилятор сможет это сделать более-менее эффективно, только если он сможет "подсмотреть" внутрь соответствующего operator=(), что может быть затруднительно, особенно если код этого оператора находится в другом модуле по отношению к модулю, в котором непосредственно производится инициализация объекта соответствующего типа, что встречается сплошь и рядом.

Это вот просто те вопросы, что сразу видны невооруженным глазом, я даже не буду касаться более интересных вопросов про operator=(T&&) и виртуальное наследование, и т.п. Эта статья - это вот чистая графомания дилетанта, я даже затрудняюсь сказать, есть ли в ней вообще хоть что-нибудь ценное, одни лишние сущности и пессимизации уже существующих и работающих решений.

P.S. Кстати да, чуть не забыл еще одну очевидную вещь, бгг.

const SomeType obj = { ... };

этот банальный и практически повсеместно встречающийся код так называемой "трехстадийной инициализации" вообще не поддается!

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

Я представлю банальный гипотетический пример. В одной версии int 4 байта, в другой версии он же уже 8 байт. Так решили ради консистентности между разными платформами. После чего вся общая совместимость по ABI не будет работать. Думаете такого геморроя хотят разработчики?

Версионность на уровне файла работает там, где компилятор может динамически определять правила поведения кода. В Dart, например, есть именно такое, что вы описываете. Но нюанс в том, что Dart, молодой язык. Он более высокого уровня, чем C++. У него нет такой кодовой базы, что есть у C++.

В вашем же комментарии по ссылке вы описываете например то, как работает сейчас C++2 от Херба Саттера. Он слой над C++. У него свои отдельный файл с расширением cpp2. И он конвертируется в C++. И именно конвертация в единый язык, позволяет двум разным языкам существовать одновременно. Но они существуют только пока C++2 соблюдает правила C++

Очевидно что не любые изменения можно версионировать, а только изменения в синтаксисе (то что называется синтаксический сахар). В одной версии функция объявляется как inf foo(int), в другой как func foo(int) int, и т.д. А как раз ABI у разных версий должен быть общим.

В результате в новой версии язык станет синтаксически чище и понятнее. Можно будет легче вводить новые языковые возможности. Это - единственная цель данного предложения. Но цель важная, так как именно этот "сахар" определяет в конечном итоге удобство использования языка программистами, количество ошибок, всяких UB и т.д.

А какой смысл в «сахаре», если он не исправляет ошибки? Основная проблема C++ сейчас - это обратная совместимость. Именно из-за этого не меняется ABI. А большинство ошибок, это артефакты прошлого

Смысл как раз в том, что новый синтаксис исправляет ошибки дизайна языка. Делает его более логичным, более строгим, более удобным. А предложенный мной путь позволяет сохранить совместимость со старым кодом (точнее, отсутствие необходимости его переписывать) и при этом писать новый код уже на новом диалекте - и всё в рамках одного проекта. Буквально, часть файлов с расширением *.cpp, часть с расширением *.cpp2, и все компилируется и работает.

Но вы же не Херб Саттер. Вы опоздали. Потому что он-то как раз сделал C++2. И он как раз делал именно так, потому что изменить C++ не получалось. При том, что сам Херб Саттер входит в комитет. И даже он пишет, что полностью избавить от проблем языка не получается, так как язык C++ имеет слишком много внутренних проблем. Через C++2 Саттер игнорирует часть проблем, но не решает. На данном этапе C++2 от Херба Саттера это просто «сахар». Который компилируется в чистый C++, скрывая часть проблем. Если вы предлагаете тоже самое, то лучше не надо. Это путь в никуда. Так как это простое игнорирование проблем ради «сахара». Я выше писал о ситуации, когда C++2 станет стандартом, вот тогда он будет к месту. Как язык Zig для языка C. Грубо описывая, когда C++2 станет самостоятельным языком, но с простой системой взаимодействия с существующих кодом на C++. И на это претендует Carbon. Который не только компилируется в C++, но и C++ компилируется в Carbon. И если в первом случае, это делается ради медленного перехода от C++ к Carbon в существующей кодовой базе. То во втором случае - это прямой слом ABI для исправления косяков языка C++

Мыши продолжают жрать кактус потому, что организм млекопитающего приспосабливается под потребляемую пищу. Это биология. Если рядом с такой мышью положить сыр, ей все равно будет хотеться кактус.

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

Когда появился win32, int-ы для виндовс-разработчиков стали 4-х байтными (были - 2-х). Я тогда как раз начинал программировать. Наверное, это создало какие-то проблемы для корпоративных разрабов, но им за это деньги платят. А сообщество энтузиастов тогда было в диком восторге. Никакие проблемы никому настроение не портили.

Куча win16 кода очень быстро потеряла актуальность. Но не потому, что стандарт С++ изменился, а потому, что Windows 3.11 сдох.

Я полностью согласен с AnimeSlave, что чемодан с уже нафиг нужным функционалом нам тащить с собой и дальше, но если каждое когда-то принятое решение (неважно, насколько удачное) мы будем беречь как "священную корову" и при этом еще добавлять новые(!)... через какое-то время нам просто понадобится другой язык.

Sign up to leave a comment.

Articles