Открыть список
Как стать автором
Обновить

Комментарии 119

А есть, кстати, соус этой картинки? Хочу сделать такое же, но про равенство в теориях типов.

У меня нету, картинка из интернетов)

Это клип из «Форреста Гампа», там про креветки вместо C++.

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

Который день сижу и плАчу.

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

Вот что говорят stackoverflow.com/questions/53250463/why-c20-doesnt-support-out-of-order-designated-initializer:
In C++, members are destroyed in reverse construction order and the elements of an initializer list are evaluated in lexical order, so field initializers must be specified in order.

Конечно, понятно. По мне, это просто делает нововведение бесполезным. Так можно было бы эмулировать тэгированные параметры функций как в C# — завести параметры по умолчанию в середине, не запоминать порядок аргументов. А тут такая шляпа.

Ну тут шляпа наполовину — пропускать элементы можно, но вот порядок сохранять придётся. У gcc и clang, кстати, различная реакция на изменение порядка следования именованных полей в списке инициализации — у gcc это ошибка, а у clang предупреждение о том, что поля будут инициализированы в порядке объявления, а не в порядке появления в списке.

Предупреждение только в clang-10. И я рекомендую таки компилировать с -Werror=reorder-init-list, потому что альтернатива — это какой-то кошмар: объекты в этом случае инициализируются дважды, удаляются (sic!) во время генериации конструктора — и хотя оптимизатор обычно это безобразие изводит… лучше на это не полагаться. Потому что, когда, например, там ссылка, то… бывает всякое.

В общем хотя clang, вроде как, позволяет переставлять аргументы… лучше этого не делать.
Ну тут шляпа наполовину — пропускать элементы можно, но вот порядок сохранять придётся.
Чего, собственно, и достаточно. Да, немного неудобно — но на читабельность не влияет, а при написании… компилятор вам поможет.
Согласен, что наполовину.

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

По сути: кому помогал тот факт, что поля класса инициализируются в порядке их объявления в классе? Это массовый источник заблуждений. Все ожидают, что они инициализированы так, как ты записал в списке инициализации конструктора, и мне до сих пор приходится себя одергивать, когда я его пишу. То, что порядок инициализации не перегружен порядком, заданным мной к конструкторе, меня удивляет. Можно себя очень глубоко загнать — godbolt.org/z/NpFiI1:

#include <iostream>

struct A { int i; A(int i) : i(i) { std::cout << i << "-a "; } };
struct B { int i; B(int i) : i(i) { std::cout << i << "-b "; } };

struct S {
B b;
A a;
S() : a(2), b(a.i) {}
};

int main() {
    S s;
}

Дальше, список инициализации в конструкторе не требует сохранять порядок как в классе, хотя оператор запятая у нас работает строго слева направо. Почему в списке инициализации можно, а в тэгированной инициализации нельзя? Может быть, комитету стоило поправить это место, и разрешить перегрузку порядка инцициализации с по-классовой к по-списочной?
То, что порядок инициализации не перегружен порядком, заданным мной к конструкторе, меня удивляет.
Но тот факт, что объекты всегда разрушаются в порядке, обратном порядку их создания вас, при этом, не удивляет? А ведь это фундаментальное свойство будет утеряно если кто-нибудь реализует ваше предложение…

Все ожидают, что они инициализированы так, как ты записал в списке инициализации конструктора, и мне до сих пор приходится себя одергивать, когда я его пишу.
Не знаю кто такие «все»: я, как бы, всегда ожидал что конструкторы и деструкторы работают по принципу FIFO и меня всегда напрягало то, что компилятор вообще принимал другой порядок. Так как сбивает с толку. Слава богу сегодня это не проблема, -Werror=reorder рулит.

Дальше, список инициализации в конструкторе не требует сохранять порядок как в классе
И вот именно это — и есть фундаментальная ошибка, которую когда-то сделал Страуструп.

хотя оператор запятая у нас работает строго слева направо.
Это тут вообще причём?

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

Может быть, комитету стоило поправить это место, и разрешить перегрузку порядка инцициализации с по-классовой к ко-списочной?
По хорошему-то нужно сделать наоборот… и сделать -Werror=reorder стандартом… но тут серьёзно пострадает обратная совместимость — так что вряд ли это когда-либо сделают.

У C#/Java этой проблемы нет, так как деструкторов тоже нет, а для финализаторов ничего не гарантируется.

C, C++ и C# — это всё разные языки. С разными свойствами. Смиритесь.
Да, согласен. Без генерации компилятором отнаследованных классов с виртуальными деструкторами здесь не обойтись, никак не запомнить порядок инициализации полей для его раскрутки при разрушении объекта. Пойдём тогда ещё дальше — насколько действительно важно это «фундаментальное свойство» в прикладном коде на С++17? По мне так это скорее исключение, когда у вас эти поля ходят как-то друг к другу изнутри через ссылки или указатели друг на друга, и вам нужно отконтролить их жизненный цикл в совокупности — развязать направленный граф, так сказать. Часто ли?
Пойдём тогда ещё дальше — насколько действительно важно это «фундаментальное свойство» в прикладном коде на С++17?
Зависит от прикладного кода, в общем случае. Так-то и код на C90 — формально кодом на C++17 является.

Если же рассматривать «идеоматичный C++17»… то это его основа. Поверх этого надстроены RAII, умные указатели и прочее, прочее, прочее. Так называемый «современный C++» чуть менее, чем полностью поверх этой концепции построен. Но да, это немного в расширительном смысле, с выходом за границы одного объекта.

По мне так это скорее исключение, когда у вас эти поля ходят как-то друг к другу изнутри через ссылки или указатели друг на друга, и вам нужно отконтролить их жизненный цикл в совокупности — развязать направленный граф, так сказать. Часто ли?
А часто ли вас вообще волнует в какой последовательности подъобъекты конструируются? В моей практике ситуация такая:
1. В подавляющем большинстве случаев мне вообще пофигу в какой последовательности подобъекты конструируются и уничтожаются.
2. Зато уже если так случилось, что мне это не всё равно… то почти всегда мне при этом нужно и чтобы и удалялись они тоже в предсказуемой последовательности.
Совершенно согласен.

Я понимаю, что RAII, copy elision и пр. полагаются на фундаментальное свойство. Я не могу ответить, как. Я хотел бы иметь этот ответ, чтобы объяснить изучающим С++: «это так, ПОТОМУ ЧТО вот», а не просто «потому что». Пока не могу. Может быть, поможете :)

Конкретно, вот например RAII. Почему ИМЕННО ему важен детерминированный порядок инициализации и разрушения?

Хочется показать 2-3 примерами, что фундаментальное свойство — так его и назовём — не вещь в себе, а требование корневых механизмов С++.
Самый элементарный пример — мутексы. Мутексы по-хорошему должны освобождаться строго в обратной последовательности по отношению в той, в которой они были захвачены, иначе могут быть всякие неприятные следствия в виде дедлоков. RAII в текущей реализации это обеспечивает автоматически. Далее, если есть ресурсы, зависимые друг от друга, например, файл и поток, то логично, что сначала должен открыться файл, потом на его основе быть создан поток, а при их разрушении наоборот — сначала должен закрыться поток, сбросить там кеши всякие и так далее, а потом уже должен закрыться файл, к которому он относится.
Спасибо. Это, правда, не «нутро» RAII, а всё же частный случай. Но будет ли в этом частном случае хорошим тоном полагаться на порядок, выбранный компилятором? Очень уж неявно — почва для ошибок. Human first, вы же понимаете. Разве не стоит сделать что-то типа:

#include <memory>

struct Resource {};
struct Mutex { Resource& r; Mutex(Resource& r) : r{r} {} };

class Owner {
protected:
  std::unique_ptr<Resource> r { std::make_unique<Resource>() };
  std::unique_ptr<Mutex> m { std::make_unique<Mutex>(*r) };
public:
  ~Owner() { m.reset(); r.reset(); } 
};

int main()
{
    Owner o;
}


Конечно, если я СЛУЧАЙНО поменяю порядок строк с r и m местами, будет беда. Захотеться случайно менять этот порядок, если я внесу его в конструктор, уже не должно. Попробуйте.

Мне кажется, очень уж самонадеянно и высоколобо было отдавать именно порядок инициализации и деинициализации на роль «RAII для класса как scope».
Так все детерминировано же в подавляющем большинстве случаев (за исключением разве что глобальных объектов со static storage duration из разных модулей) — и в каком порядке вызываются конструкторы, и в каком деструкторы, причем все сделано, так сказать, естественным образом — конструкторы в порядке объявления соответствующих переменных, деструкторы в порядке, обратном вызову конструкторов. Не вижу тут проблемы, все сделано естественно и удобно.
Я вижу, и не я один. Случайно поменять порядок объявлений очень легко, особенно если работаешь не только с С++. Варнингов типа «warning: order of member declarations changed since last time» у нас нет и не будет. Потому — defensive programming, явно задаём порядок кодом, если он нужен, не полагаемся на сомнительную фичу. Она безусловно детерминирована, но настолько неявна, что лучше её полностью игнорировать.

Я совершенно понимаю, что они хотели ей сказать — что, мол, типа, раз в куске кода переменные у нас создаются на стеке в порядке их появления в коде, то пусть так будет и в scope класса, круто же, единообразно? Но, нет, не круто. Люди не смотрят на класс как на линейный код, потому что класс — не линейный код. Это НЕУПОРЯДОЧЕННЫЙ набор полей и методов.
Хозяин — барин :) Никто не мешает использовать defensive programming. Но и в детерминированности относительно порядка инициализации и уничтожения в данном случае я ничего плохого тоже не вижу.
А потом начинается ещё… Поля в структурке перетасовать, чтобы утрясти размер, а тут порядок понимаешь ли, и т.д. и т.п. Знай сиди и рефактори до бесконечности свои назначенные инициализации :)
Ну так вы уж определитесь — либо у вас класс это «НЕУПОРЯДОЧЕННЫЙ набор полей и методов», либо вы хотите «поля в структурке перетасовать».

Первое со вторым, как бы, не очень совместимо…
Дык я только и хочу их перетасовать, что у C++ такие правила выравнивания :) У нормальных людей класс — это логическая единица, неупорядоченный набор полей и методов. В С++ класс имеет мало общего с этим определением, т.к. он сразу тебя окунает в свою физику.
Спорить об истинных шотладцах я не собираюсь, извините.

Есть и языки, где в класса во время работы программы могут появляться и исчезать поля во время работы программы (см. MOP) — и именно таким был ООП изначально.

Считать ли теперь C# и Java «ущербными» из-за того, что там это невозможно сделать? Вопрос, как обычно, философский.

Класс в C++ — это упорядоченная последовательность полей и методов. И их порядок — важен. Как бы вам ни хотелось чего-то иного.
Она безусловно детерминирована, но настолько неявна, что лучше её полностью игнорировать.
От умных указателей вы тоже, стало быть, отказываетесь? Там же неявно где и как они память освободят!

Это НЕУПОРЯДОЧЕННЫЙ набор полей и методов.
Нет, нет и нет. Почитайте правила, хотя бы:

You cannot…
    For virtual member functions
        change the order of virtual functions in the class declaration.

А уж про добавление/удаление полей я вообще молчу: поля (даже приватные) — это часть интерфейса класса. И, разумеется, их порядок важен.

Да, в C++ есть вот такие вот, несколько странные и неожиданные «правила игры» — но это плата за эффективность.

Какой смысл платить за них, но не использовать? Для этого другие языки есть…
Я как раз об этих правилах. Виртуальная функция — это типа указатель, со всеми вытекающими. Снова возвращаемся к фундаментальному правилу. Это мне и вам понятно. Понятно ли начинающим? Отнюдь. Все нормальные люди и начинающие считают класс НЕУПОРЯДОЧЕННЫМ множеством. ООП. В C++ же оно оказывается скорее упорядоченным, нежели нет. Скажем, частично и упорядоченным. Об этом кричат? Нигде не встречал. :) Хотя, о чём кричат на CppCon и иже с ним — о том, что «нам учить других этому языку, и с этим гигантская проблема».
От умных указателей вы тоже, стало быть, отказываетесь? Там же неявно где и как они память освободят!
А почему надо отказываться? .reset( в нужном порядке ему, счётчик ссылок далее позаботится об удалении. В том примере выше я тоже ничего не удалял delete-ом, хотя вызов reset этому эквивалентен, но порядок удаления указал совершенно жёстко. Или вы о порядке для класса, содержащегося в shared_ptr-объекте?
Я о том, что момент, когда будет освобожден объект, указатель на который хранится в unique_ptr определяется ровно-таки такими «нинужнами» правилами вызова деструкторов.

И да, можно объединить недостатки C++ и C явно вызывая эту операцию с помощью .reset(nullptr) — вот только зачем в этом случае вообще нужны умные указатели — неясно совершенно.

Вообще неясно зачем при использовать язык, основным и главным достоинством которого являются неявные операции (неявное преобразование типов, неявный вызов конструктора и деструктора и так далее) и носиться с лозунком «явное лучше неявного».

Хотите всего явного — есть же C, чёрт побери!
.reset( в нужном порядке ему, счётчик ссылок далее позаботится об удалении.

Теперь у вас одна и та же информация продублирована дважды — в объявлении класса и в объявлении его деструктора.


Так, конечно, лучше.

Там ещё гораздо веселе получается (я ссылки на godbolt приводил): из-за этого дурацкого resetа компьютеру приходится выполнять больше работы, что выливается в лишний код — и да, я могу привести весьма патологическую программу, которая будет от этого кода зависеть, так что это не «недоработка в компиляторе» — это сознательно написание плохого, плохо оптимизируемого, кода.
Да, в C++ есть вот такие вот, несколько странные и неожиданные «правила игры» — но это плата за эффективность.
Я, конечно, буду здесь голословным, ибо в комитет, к сожалению, не вхож. Если бы ребята алгебраически подошли к проблеме построения системы типов, то пруверы им бы указали на все эти недочёты и промахи, которые они продолжают вносить и исправлять, вносить и исправлять. Я таки не понимаю, как бинарная несовместимость классов с разным порядком одинаковых членов (по сути это всё разные классы бинарно) связана с эффективностью.
Я таки не понимаю, как бинарная несовместимость классов с разным порядком одинаковых членов (по сути это всё разные классы бинарно) связана с эффективностью.
Вы действительно не понимаете? Или притворяетесь, что не понимаете?

Ну вот простейший пример:

struct Foo {
  std::vector<int> array1;
  std::mutex mutex1;
  std::vector<int> array2;
  std::mutex mutex2;
};
struct Bar {
  std::vector<int> array1;
  std::vector<int> array2;
  std::mutex mutex1;
  std::mutex mutex2;
};
Вы хотите сказать, что не понимаете почему Foo более эффективен, чем Bar?

Или тут:
struct Foo {
  long x1, y1, z1;
  long x2, y2, z2;
  std::string title;
};
struct Bar {
  long x1, x2;
  long y1, y2;
  std::string title;
  long z1, z2;
};
Тоже загадка?

Если бы ребята алгебраически подошли к проблеме построения системы типов, то пруверы им бы указали на все эти недочёты и промахи, которые они продолжают вносить и исправлять, вносить и исправлять.
Пруверы могут лишь что-то сказать про модель мира, которая у них есть. А у разработчиков C++ как раз проблемы с описанием этой модели, извините.

Такой пример:
struct Foo {
  long x1, y1, z1;
  long x2, y2, z2;
  std::string title;
};
struct Foo {
  std::string title;
  long x1, y1, z1;
  long x2, y2, z2;
};
Какой из классов быстрее? Неужели думате, что без разницы? Нет — разница таки есть… только вот в зависимости от программы быстрее может быть как Foo, так и Bar. И куда вы это в своём «прувере» засовывать собрались?
НЛО прилетело и опубликовало эту надпись здесь

Всё дело в выравнивании полей и особенностях невыровненного доступа на различных архитектурах.

Нет, дело не в вырванивании, тут всё во всех структорах выравнено. Вопрос в кешировании и взаимодействии вот этого вот всего с кеш-линиями. Заметьте, кстати, что в последнем случае не будет никакой разницы, если вы положите эти структуры в массив, выигрыш будет если вы будете выделять память в стиле Java — отдельно для каждой структуры. Причём ведь ещё и окажется что от malloc'а это зависит!

В любом случае — это вещь, которую ни компилятор, ни формальный «прувер» не в состоянии правильно запроектировать. Потому что всё зависит от структуры вашей программы. Причём не локальной, а глобальной.

И никто, кроме вас не будет знать что для вас полезнее — увеличить размер структуры, но добиться того, чтобы данные лучше ложились в кеш-линии, либо уменьшить — и добиться, чтобы страктуры не вытесняли другие данные из L1.

Дык, выравнивание по размеру кэш-линии.


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


Конкретно в моём случае возня с кэшем играла роль только при эффективной реализации математических алгоритмов.

Заметьте, кстати, что в последнем случае не будет никакой разницы, если вы положите эти структуры в массив, выигрыш будет если вы будете выделять память в стиле Java — отдельно для каждой структуры. Причём ведь ещё и окажется что от malloc'а это зависит!

А я что-то про false sharing подумал. Типа, разнести независимые вещи подальше друг от друга.


И вы, кстати, во всех этих случаях alignas(64) забыли. sizeof(T) == 64 не гарантирует помещения всего значения в одну кешлинию.

sizeof(T) == 64 не гарантирует помещения всего значения в одну кешлинию.
Там размер как раз не 64 ни в одном из вариантов. И именно потому, что там не написано alignas(64) — большинство маллоков разместят эту структуру так, что, противоестественным образом, версия со string вначале будет быстрее, если мы будем часто обращаться к координатам… и наоборот, она будет медленнее, если мы будем чаще отращаться к строкам.

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

Значит ли это, что нельзя сделать лучше, чем в C++? Нет, не значит, в Rust сделано лучше: обычно компилятор подбирает layout структур сам, «способом, приближенным к оптимальному», но если очень надо — layout можно задать самому, как в C/C++.

Нужно ли это фичу тащить в C++? Не уверен: практически в 99% случаев заметной разницы между «тупым» варинтов, когда вы размещаете поля так, как вам удобнее о них думать и «способом, приближенным к оптимальному», как в rust — не будет, а там где это окажется важно — компилятор, скорее всего, не очень-то и справится.

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

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

Доступа к полям?
Ну в первом случае это очевидно: раз у нас два независимых мьютекса, то большой шанс, что они будут браться независимо. И, стало быть, нахождение их в одной кеш-линии — крайне нежелательно. Хотя можно себе представить и патологическую программу, где всё наоборот.

Аллокации пустой структуры?
Тоже варинт. Странный, но возможный.

Ее заполнения?(если это пункт, то как заполняется строка?)
Это может быть как строка с сообщением об ошибке (в большинстве случае пустая), так и ключ поиска (к которому обращаются чаще всего).

Да, вы правы — без знания того, что, кто, когда и как делает с этой структурой не только бенчмарки бессмысленны, но и вообще — неясно под какой паттерн доступа к памяти мы всё это оптимизировать хотим.

И если бы были заданы эти вопросы — претензий бы не было. Но ведь от меня просят объяснить не это. А просто показать бенч. Ну состряпаю я его — дальше что? Что и кому это покажет?
Это физика. Меняю порядок полей для оптимизации — почему это должно сказываться на физике назначенной инициализации? Почему эта локальная оптимизация (делаю поля соответствующими схеме доступа к ним) должна меня заставлять перетасовывать прочие места кода? И вообще, по-хорошему, переупорядочить поля согласно паттерну доступа, кто там первый загрузится в кэши с кодом и т.д. — это задачка для PGO.
И вообще, по-хорошему, переупорядочить поля согласно паттерну доступа, кто там первый загрузится в кэши с кодом и т.д. — это задачка для PGO.
Ну вот как вы сделаете это — так и будет о чём поговорить. Языков, подобные вещи допускающих есть у нас. Тот же rust.

Так-то рассказы на тему «а чёт-то мне звезду с неба не сняли и на блюдечке с голубой каёмочкой не поднесли» каждый может сочинять… вот только компиляторы от этого не образуются…

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

О пруверах я всегда рад поговорить, но у чуваков из комитета есть один очень сложный и всё ломающий констрейнт: совместимость с С.


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


Я таки не понимаю, как бинарная несовместимость классов с разным порядком одинаковых членов (по сути это всё разные классы бинарно) связана с эффективностью.

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

Мне кажется, очень уж самонадеянно и высоколобо было отдавать именно порядок инициализации и деинициализации на роль «RAII для класса как scope».
По-моему самонадеянно ходить в чужой монастырь со своим уставом. Вот вы в вашем примере поорождаете кучу ненужной деятельности… почему, зачем? Ну вот сравните ваш C#/Java-стиль с C++ стилем — 160 лишних, никому не нужных байт, 50 лишних, никому не нужных команд… только на создании и удалении объекта… это если забыть про то, что мы ещё и «в куче» лишнюю память аллоцируем. Да даже ваши никому не нужные resetы убрать — это уже даст 48 байт экономии в 10 инструкциях (да и быстрее будет).

Вообще ваша проблема в том, что вы, с одной стороны, хотите использовать C++ потому что он быстрый и эффективный, а с другой — вы его ненавидите потому, что он неSOLIDен!

Но блин, неужели же вы не понимаете, что SOLID (при всех его положительных чертах в смысле удобства поддержкания кода и прочего) — страшно неэффективен? Идеоматичная программа на C#/Java в типичном случае медленее идеоматичной программы на C++ не потому, что C#/Java медленные JIT'ы имеют! Программа, написанная на C#/Java с учётом особенностей CPU может и быстрее программы на C++ быть!

Соответственно чтобы не обижаться на комитет по стандартизации C++ нужно просто выбрать: хотите вы быстрый язык… или SOLIDый! И всё, проблема решится.
Хорошо, фиг с ним, с этим поинтером. Согласен, поинтер перебор. Облегчим структуры. Что хотел показать тем примером: godbolt.org/z/OVirOQ (моё) / godbolt.org/z/LFds9y (ваше). Один-в-один. Собственно, я хотел явности в порядке, оно теперь у меня есть. Совсем по-хорошему нужен ещё свой std::reference_wrapper, который бы можно было инициализировать nullptr с дебаг-ассертами на обращение к нему пустому, чтобы не ничего не делать с сырым ptr. Да, здесь я, безусловно, борюсь с языком, лишь бы не полагаться на «фундаментальное правило», ожидая такой защитой, что кто-то о нём точно забудет. Что ж поделать, раз так задизайнили язык.
Собственно, я хотел явности в порядке, оно теперь у меня есть.
Ну да. А ещё у вас есть возможность напутать в вызовах. И не вызвать в нужном месте release. Ну и? Чего вы этим, собственно, добиваетесь?

Да, здесь я, безусловно, борюсь с языком
А зачем?

ожидая такой защитой, что кто-то о нём точно забудет.
Это вряд ли. То есть заставить кого-то выкинуть ваш код и написать нормальный, читабельный, наверное кого-то вы и сможете побудить (хотя зависит от того, насколько этот ужас его «достанет»). А вот забыть о том, что C++, всё-таки, должен выглядеть по другому… это вряд ли.

Что ж поделать, раз так задизайнили язык.
Выбрать другой язык? С тем же успехом можно сказать, что в английском и русском глагол «не там» стоит — и начать разговаривать в стиле Йоды. Ничего, кроме смеха вы, таким образом, не добъётесь.
Повторюсь — я отлично понимаю, чего добивались таким дизайном авторы языка. Решили не плодить сущности без необходимости, встроили порядок инициализации в порядок объявлений. Окей. Я считаю, это ошибка — вписывать низкоуровневый аспект физического дизайна в логическую структуру кода. Нужна была другая сущность. Это всё можно сделать zero-cost, конечно же.

Более того, я считаю, что именно этим правилом они и порочат zero-cost abstraction принцип. Цена этой абстракции — что с новыми фичами код теперь надо причёсывать на каждую перегруппировку переменных. Цена — производительность итоговой программы ценой этой вот возни, стоящей времени программистов, когда как ту же производительность можно было бы сохранить введением явного означения порядка как отдельной сущности. Да и даже улучшить — некоторые, например, просто не пожелают лишний раз перегруппировывать порядок определений, дабы не рефакторить массу кода.
Ну хотя бы:
class Owner {
protected:
  BigObject o;
  Resource r;
  Mutex m {r};
  [[order:r,o]] // order r->m will be derived automatically, s.t. it becomes [[order:r,m,o]] by the compiler
};

И мне это даёт всё сразу. Компилер выведет порядок r->m, потому что я явно о нём пишу. Сейчас я могу переставить две эти строки и получится ерунда. Для o я ПОЧЕМУ-ТО хочу, чтобы o шло после r. Пожалуйста — задаю это. Всё явно, компактно, и нет прочих требований, связанных с тем, что этот порядок будет влиять на то, как вы выписываете код, использующий Owner. Я не понимаю, почему развязку и наполнение ациклических графов зависимостей нельзя возложить на компилятор, и я должен этим заниматься.

Естественно, когда задизайнили это нынешнее правило с порядком, не было аннотаций, как минимум. Но чем они думали (или может быть даже ОН? не упомню, писал ли он об этом конкретно аспекте в Design and Evolution of C++), когда видели, что даже когда r явно зависит от m, компилятору на это по боку, и язык ничего не требует. Бред же. Вы защищаете плохой дизайн, я считаю.
Тут ещё что страшно в языке:
class Owner {
protected:
  Mutex m{r};
  Resource r;

Хоть у меня там всё НА ССЫЛКАХ, это таки скомпилируется. Даже ворнинга не приходит. И это страшно. Конечно, можно верить, что дураки не прикоснутся к вашему коду и т.д. Но жизнь расставляет всё по местам.

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

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

Фичи в языке появляются не по желанию чьй-то левой пятки. Кто-то должен написать попозал, провести оценку полезности, убедить комитет в том, что решаемая проблема — таки реальна… продумать как всё это будет согласовываться с другими фичами языка… и да, вот после всего этого — можно и в стандарт включить…

когда видели, что даже когда r явно зависит от m, компилятору на это по боку, и язык ничего не требует. Бред же.
Почему бред? Не вижу ничего странного в том, что метка текстового поля будет знать о своём текстовом поле. Просто она не должна будет, в этом случае, пытаться обращаться к нему в конструкторе и деструкторе — но само-по-себе знание вполне законно.

Вы защищаете плохой дизайн, я считаю.
Я не «защищаю» «плохой» дизайн. Я объясняю почему он таков, каков он есть.

Хотите другого дизайна? Вперёд! Языков в мире — тысячи, если не миллионы. Можете и свой создать.

Но делать вид, что язык устроен не так, как он устроен и «бороться» с ним… ну что за детский сад, ей богу… назло бабушке отморожу уши…
И? Как в данном случае выглядел бы initializer list?
Да уж и совсем в простом.

struct Data {};

struct Host {
  Data& d;
  Host(Data& d) : d{d} {}
};

// this WILL compile ;(
struct Mixed {
  Host h{d};
  Data d;
};

int main()
{
  // this won't compile
  // Host h{d};
  // Data d;
}

Это комитет расстреливает нам ноги.
Это комитет за вас этот код пишет? :) И, кстати, ничего такого страшного в нем нет. d в Host будет проинициализирован ссылкой на d из Mixed, вот и все, все вполне валидно. До тех пор, пока вы в конструкторе Host по этой ссылке не полезете в еще не инициализированные внутренности этой d из Mixed, ничего страшного не случится.
Не за меня. Так я ж не все. Может я конечно упустил, где в C++ Guildelines этот момент ловится, или clang-tidy может словит? В любом случае — это намеренное проталкивание ногострелов, когда без них можно было обойтись без какой бы то ни было потери производительности кода.
Что именно он должен словить? Еще раз — код, который вы привели выше, вполне валидный.
Так уж и не додумаете? :) Хорошо: godbolt.org/z/KuYMzu. Ронять не буду — но вы поняли. Меняю местами объявления — получаю 3 на выходе вместо 1.
Там не тот код, что в вашем комментарии выше. Я же написал — «до тех пор, пока вы в конструкторе Host по этой ссылке не полезете в еще не инициализированные внутренности этой d из Mixed, ничего страшного не случится». А вы полезли.

P.S. Вы занимаетесь манипуляциями. Это все равно, что назвать «проблемным» код типа такого:

int foo()
{
    int f = 1;

    return f;
}


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

int &foo()
{
    int f = 1;

    return f;
}


и вы такой: «ага-ага, видите-видите, упало!» :)
На ваш пример -Wreturn-local-addr -Wall же. А вот решить ошибки с порядком объявлений без санитайзеров уже никак.
Вопрос не в том, кто чего ловит. Вопрос в том, что вы либо не разбираетесь в предмете, либо сознательно всех путаете.

В вашем примере выше никаких проблем не было. Ну вот совсем никаких. А вашем примере, который упал — были. При этом вы упорно делаете вид, что это — эквивалентные примеры.

Вывод: вы либо не знаете C++, либо троллите. Судя по остальным комментариям — я склоняюсь ко второму.
Я не могу принять вот эту запись беспроблемной:
struct Data {};

struct Host {
  Data& d;
  Host(Data& d) : d{d} {}
};

// this WILL compile ;(
struct Mixed {
  Host h{d};
  Data d;
};


Она компилируется, но может привести к неочевидным последствиям в будущем (при развитии кода классов). Это было показано в последующих примерах.
Справедливости ради, конструктор Host получает и сохраняет ссылку, которую можно будет потом (после завершения конструктора) использовать. В некоторых случаях, ничего криминального.
Скрытый текст
struct Data { int a = 42; };

struct Host {
  Data& d;
  Host(Data& d) : d{d} {}
  void use() { printf("%d", d.a); }
};

struct Mixed {
  Host h{d};
  Data d;
};

int main()
{
  Mixed().h.use();
  return 0;
}

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

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

Да, С++ не пытается диктовать вам условия. И ошибок типа «illegal forward reference» Java в нём нет — но, кстати, уже сами эти ошибки показывают нам, что класс — это, всё таки упорядоченный набор полей даже в Java. Кстати там тоже можно получить вариант, когда порядок объявления полей важен даже в программе, которая компилируется. Легко.
Что вы всё про меня-то. Я показываю гипотетическую ошибку. Не мою. Её непросто отловить из-за дизайна языка.
Я показываю гипотетическую ошибку.
Не надо показывать «гипотетическую ошибку». То что пользоваться C++ сложно и подобные вещи не так просто отловить — причина появления пачки санитайзеров… ASAN/TSAN/MSAN/UBSAN/DFSAN/LSAN… это всё оттуда.

Также, во многом из-за этого, в качестве альтернативы разработан Rust… который пока C++ не заменил, но имеет шансы (напомню, что C был разработан в 70е, но ещё в середине 80х куча коммерческого софта, включая аж целую известную операционку писалась на Pascal… замена языков — вообще небыстрое явление).

Вы же предлагаете вместо этого отказаться от всех преимуществ, которые даёт такой подход, одновременно продолжая платить за все его недостатки — и ради чего? Чего вы этим хотите добиться?
С другой стороны, а если кто-то гениальный много пользуется перекрёстными ссылками на ещё не сконструированные объекты, и у него всё корректно написано
Скрытый текст
struct node
{
        node* parent; node* left; node* right;
};

struct mytree
{
        node root { nullptr, &a, &b };
        node a { &root, &a1, nullptr };
        node a1 { &a, nullptr, nullptr };
        node b { &root, nullptr, nullptr };
};

А вы своим предложением разруливать автоматически порядок инициализации всё сломаете, корректный код просто перестанет компилироваться — нехорошо…
Почему не скомпилируется? Здесь циклическая зависимость. Ворнинг было бы хорошо о циклической зависимости, раз. Дальше можно было бы выбрать порядок a1, b, root, a, исходя из числа входящих рёбер — сперва инициализируем менее зависимые. Перегрузить порядок — через аннотацию.

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

Хочется показать 2-3 примерами, что фундаментальное свойство — так его и назовём — не вещь в себе, а требование корневых механизмов С++.
Вы путаете причину и следствие. Нет никаких физических законов, заставляющих проектировать C++ так, чтобы деструкторы вызывались строго в обратном порядке. Но если мы так сделали и так спроектировали язык — то дальше к нему все привыкают и на него навешивается масса конструкций.

Примеры вам привели. И да, пример, который вы придумали сами (когда объекты «живут» внутри другого объекта, при этом имеют так же ссылки и друг на друга) — тоже не редкость. Скажем метка и поле ввода, помеченное этой меткой. Не так важно — кто из них создаётся и удаляется первым… но после того, как вы об этом договоритесь (допустим первым удаляется поле ввода) то вам потребуется, чтобы это правило никогда не нарушалось (так как, напрмер, поле ввода будет «иметь право» обращаться к метке).

Пришельцам из C#/Java это тяжело понять, но код в конструкторе в C++ — отнюдь не является редкостью (в отличие от финалайзеров в C#/Java)… и это происходит именно потому, что язык даёт гарантии на тему того, когда они вызовутся.
Ну как. Я живу в C++ с 2001 года, а вот к .NET и Python лишь захаживаю. Привыкнуть за 18 лет к странному дизайну с порядком инициализации, заданном именно порядком декларациями полей, как-то не довелось и не желалось. Вернее, ни разу не доводилось на него полагаться — в случаях зависимостей и нужности порядка задавал его явно. Явное лучше неявного. Теперь же эта «фича» ещё и продолжает ограничивать развитие языка.

А вот наблюдать за адекватным развитием некоторых языков доводится с завистью. Хочется все 18 лет писать менее многословный прикладной код на С++ без птичьего щебета, запоминания порядка переменных в уме и мрака вроде www.boost.org/doc/libs/1_62_0/libs/parameter/doc/html/index.html. Экстраполировать отсутствие естественных фич в языке многоэтажным метапрограммированием — это как-то не очень.
Вернее, ни разу не доводилось на него полагаться — в случаях зависимостей и нужности порядка задавал его явно.
Но… зачем?

Явное лучше неявного.
Ещё раз: не путайте python с C++! В C++ очень много чего происходит неявно, стараниями компилятора. Это — почти что строго противоположный подход.

запоминания порядка переменных в уме
А зачем его в уме-то запоминать? Компилятор ошибку выбросит, вы исправите… делов-то. IDE, опять-таки, подсказку может показать…

Экстраполировать отсутствие естественных фич в языке многоэтажным метапрограммированием — это как-то не очень.
Не вижу ничего «естественного» в бардаке. Я даже в python стараюсь передавать параметры в том порядке, в котором они идут в описании функции. Хотя там их, как раз, можно их переставлять. То, что C++ этого требует (во всяком случае у нас, при использовании -Werror=reorder) — мне скорее нравится. Больше порядка.
Ещё раз: не путайте python с C++! В C++ очень много чего происходит неявно, стараниями компилятора. Это — почти что строго противоположный подход.

Извините, я считаю, порядок ради порядка не имеет смысла. Компилятор за вас может упорядочить аргументы из неупорядоченного списка-множества. Зачем вручную делать работу компилятора? Это же противоречит идее С++ — вы сами выше говорите, «компилятор много чего делает за нас», так? Так.

Была бы моя воля — не юзал бы Питон вообще кроме как в билд-системе, и делал бы нейронные сети на C++. Но из-за ограничений языка всё это получается настолько многословным, что деревья теряются в лесу.

А зачем его в уме-то запоминать? Компилятор ошибку выбросит, вы исправите… делов-то. IDE, опять-таки, подсказку может показать…
Ну я что могу сказать. Если бы я продавал часы — это мне всё на руку было бы. Как можно хорошо плюсануть к ивойсу это всё. Но так как я сам себе заказчик, а замены С++ по эффективности нет, вся эта мышиная возня мне сооовсем не на руку.

Я хочу элементарно писать код в pythonic-стиле, то бишь — прозрачно, компактно. Мы получаем плюшки в эту сторону, не скрою. Сколько мы их ждали, правда — другой вопрос. Поэтому, безусловно, в 2019 я могу себе позволить писать С++-код в куда более Pythonic-стиле, чем в 2011.
Но так как я сам себе заказчик, а замены С++ по эффективности нет, вся эта мышиная возня мне сооовсем не на руку.
А почему «замены С++ по эффективности нет» — никогда не задумывались? Или считаете, что оно само так, случайно, произошло?

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

int foo(int, int, int);
int foo(int arg1, int, int arg3);
int foo(int implName, int gen, int ver);

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


int foo(int arg);
int foo(double otherArg);

...

foo(otherArg = 10);

Какая перегрузка должна быть вызвана?


А если я теперь переименую аргумент в первой перегрузке с arg на otherArg?


Но это цветочки, ща ягодки будут.


template<typename F>
void sfinae_my_ass(F f, decltype(f(otherArg = 10))* = nullptr);

Класс. Мы научились определять в компилтайме имя параметров метода и делать по ним перегрузку. Вы способны просчитать влияние этого на весь стандарт? Вы уверены, что теперь имена аргументов не протекут в ABI? Я — нет, на оба вопроса. Зато я точно знаю, что это просто машина для генерации ODR violations.

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

Всё это, в принципе, разрешимо — но очень сильно меняет весь язык и совершенно неясно: насколько это реально облегчает жизнь.

В следующем смысле: да, я знаю что работать с API типа Win32 API, где у каждой функции по 10 аргументов, многие их которых — это структуры с кучей полей… неудобно и сложно… но может быть вместо кардинальных изменений языка лучше — не создавать таких API без крайней необходимости?

Я тут случайно ещё подумал, что теперь непонятно, что делать в коде


int otherArg = 0;
f(otherArg = 10);

Вызвать по именованному параметру? Положить 10 в локальную переменную и передать её как неименованный параметр в f? Совместить эти варианты? Что-то ещё?

Скорее всего что-то ещё. Возможен такой синтаксис: f(.otherArg = 10)… По аналогии с инициализацией структур.

Только вряд ли это кому-нибудь нужно: структуры в C++20 уже дают почти такую запись, а то, что порядок помнить нужно — ну так компилятор подскажет, а для читателя это, скорее, плюс, чем минус.
>> Компилятор за вас может упорядочить аргументы из неупорядоченного списка-множества.

Как бы вы на месте компилятора упорядочили аргументы тут?

A(B &&b)
    : a(b)
    , b(std::move(b))
{}


так

A(B &&b)
    : a(b)
    , b(std::move(b))
{}


или так?

A(B &&b)
    : b(std::move(b))
     , a(b)
{}
Java поступает просто: подобные вещи == ошибка компиляции.
Примеры вам привели. И да, пример, который вы придумали сами (когда объекты «живут» внутри другого объекта, при этом имеют так же ссылки и друг на друга) — тоже не редкость.

Кстати в расте так нельзя сделать совсем, и я думаю в том числе потому что нету этого провалила про порядок полей в структуре

Как бы вы упорядочивали переменные в классе?
А если дано 2 конструктора?
class A {
    int a;
    int b;
   A()
   : a(0)
   , b(1)
   {}

    A(int a)
    : b(0)
    , a(a)
    {}
};


В каком порядке тут конструировались бы прееменные?

В разделе про агрегатную инициализацию:


Этот синтаксис работал ещё в С и С++98, причём, начиная с С++11, в нём можно пропускать знак равенства:
Widget widget{1, 3.14159};

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


struct Widget {
  int i;
  int j;
};

struct Thingy {
  Widget w;
  int k;
};

int main() {
  Thingy t{1, 2}; //Error in C++11, brace elision is not allowed, ok since C++14
  return t.k;
}

Однако, GCC это всё же компилирует для С++11, но для С++98 отказывается.


UPD:


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

Как бы да, но только при наличии какого-то рантайма. На bare metal придется самому на этот счёт заморочиться.

Кстати, если сравнить с примером повыше (где в Thingy два Widget-а), непонятно, почему не разобрана такая ситуация:


struct Widget {
  int i;
  int j;
  Widget(int) {}
};

struct Thingy {
  Widget w;
  int k;
};

int main() {
  Thingy t{1, 2}; // Что вызывается?
  return t.k;
}
А в чем здесь неоднозначность? В упомянутой строчке производится aggregate initialization структуры из 2 элементов, первый элемент (Widget w) агрегатом не является (так как у Widget есть user-defined constructor), этот конструктор не explicit, соответственно он и будет вызван.
Класс, прикольно осознавать, что был на докладе того же чувака на эту тему вживую на небольшом митапе в Мюнхене :)
А где в Мюнхене такие митапы проходт?
Ну конкретно этот в офисе Джетбрейнс был. А вообще — митап дот ком :)

В разделе про назначенную инициализацию (designated initialization):


в С++ этот вид инициализации нельзя использовать с массивами. Но, опять-таки, я не думаю, что это вообще следует делать.

В GCC есть расширение (а может это даже часть С11, я не проверял), которое позволяет писать на Си вот так:


//
// Условный lib.h
//
enum HandlerType
{
        WRITE_HANDLER,
        READ_HANDLER,
        EXIT_HANDLER,
        ....
        ENUM_SIZE
}

typedef void(*HandleFunc)(void*);
typedef HandleFunc Handlers[ENUM_SIZE];

void init_сustom_lib(Handlers* handler); //Условная библиотека с коллбеками

//
// main.c
//
#include "lib.h"

void read_handler(void*){}
void exit_handler(void*){}

int main(void)
{
        Handlers handlers = 
        {
                [READ_HANDLER] = read_handler,  
                [EXIT_HANDLER] = exit_handler      

                //Write handler нас не интересует
        }; 

        init_lib(&handlers);
        //....
}

Очень удобный синтаксис на мой взгляд и вполне мог бы пригодиться и в С++. Кстати, совсем не зависит от порядка элементов в квадратных скобочках.

Это всего-навсего C99, даже не C11. И как, обычно, если его прикрутить к C++ и присывать ссылками… компилятор породит код, который завернёт вас всё программу в бараний рог.

По крайней мере clang.
Синтаксис удобный, но конкретно в этом примере структура наверное лучше подойдет.

Кстати интересно почему его не спешат в кресты добавлять, боятся усложнять парсер из-за неоднозначности с синтаксисом лямбд?
Если запретить описывать значения строго по порядку, то он не особо и нужен, а если разрешить — то возникают противоречия в правилах.

И да, в большинстве практически важных случаев можно использовать структуры.
int i4 = int();         //inits with 42

А разве не нулем?
А еще вот это одно и то же:

int i14(7, 9);          //compile-time error
int i17(7, 9);          //compile-time error


Не говоря уже о том что мне вообще непонятно как можно некомпилирующийся код посчитать за «способ инициализации переменных».
Там еще и откровенно не являющийся инициализацией пример дан.
int i13();              //declares a function


Если погромист ответственный пацан, то он пальчиками должен проинициализировать каждую переменную, обьект etc. Ибо при переходе на новую версию компилятора/компьютера/операционной системы возможно int i при первом обращении будет не нуль и даже не 255 в восьмибитных или 2^64 в соответствующих системах. И тогда шагай step-by-step ищи, где собака порылась. И не надеяться на порядочность компиляторописателей. Они тоже люди. Просто выносим инициализацию в include и все дела. Иначе старый добрый С++ превратится в Васик с непредсказуемыми результатами runtime. Я так думаю(с)(Фрунзик М.)

Не надо так думать. Да, в компиляторах есть ошибки — но предоставьте уж их разработчикам компиляторов.

Попытка вот так вот «удушить всё» в краткосрочной перспективе ничего не улучшает, а в долгосрочной перспективе ухудшает: если таких умных, как вы, оказыватся много, то компилятор учат ваш код игнорировать, в этом игрировании обнаруживаются ошибки… ну и дальше — классика, гонка вооружений…

Но тем не менее, явное лучше не явного.

Это вы С++ с python перепутали. И то — даже там есть with.

А C++ чуть менее, чем полностью, состоит из «неявного»: всякие RAII, хитрные вызове конструкторов и прочее.

Хотите, чтобы всё было явно — программируйте на C.

Да на самом деле бесит что примитивные типы ведут себя не так как нормальные классы. Почему конструктор по умолчанию не определен у int?

Чтобы не платить за то, что не используется.
unsigned bytesRead; // тут не нужна инициализация нулём
ReadFile(hFile, buffer, len, &bytesRead, NULL);

а не лучше


unsigned bytesRead = ReadFile(hFile, buffer, len);

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

Я бы рад, но это функция Win32 API, и как-то повлиять на её сигнатуру уже невозможно.
а об ошибке как тогда тогда узнать?

std::optional как вариант или std::tie. Я не знаю, сам писал (немного) раньше в си стиле. Интересно как принято сейчас делать в плюсах, разве не затем придумали исключения и все остальные штуки поновее?

Это всё хорошо для нового модного кода.
Но как вам ситуация, когда в C++22 ввели наконец принудительную инициализацию всех простых типов и после перекомпиляции во всех старых программах, написанных ещё в C99-стиле, просела производительность?

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

Если хорошо подумать, новый код можно писать так, чтобы лишней инициализации не было. Но об этом надо заранее заботится, и, возможно, некоторым API потребуется рефакторинг.
Но если необходимо просто вызвать конструктор, то лично я предпочитаю использовать прямую инициализацию, то есть классический синтаксис. Я прекрасно понимаю, что в этом со мной многие не согласятся — Николай говорил, что предпочитает для этого использовать фигурные скобки. Мне кажется, что круглые скобки более очевидны, поскольку тогда синтаксис такой же, как при вызове функции, и сразу ясно, что выполняется разрешение перегрузки. Все правила здесь очевидны, не надо думать, есть тут или нет initializer_list. Мне этот подход кажется более простым и ясным: когда смотришь на такой код, сразу ясно, что он делает.

В обычном случае можно использовать то что нравится, но при использовании круглых скобок следует помнить, что порядок вычисления аргументов внутри не гарантирован.
Для фигурных же он определён слева направо.
То чувство когда решаешь свои задачи в том числе с помощью C++, а он развивается и уплывает от тебя куда-то далеко и, возможно, в будущем не сможешь не только читать современный C++, но и использовать C++ вообще

А есть что-то такое, но про указатели и выделение памяти? Много раз слышал, что использовать сырые указатели и new не стоит, но что сейчас правильно использовать?

У нас есть 14 разных способов инициализации переменных. Надо разработать один универсальный.

Теперь у нас есть 15 способов инициализации.
Спасибо за материал.
Тот самый случай, когда понимаешь, откуда есть пошли все мемные картинки про «изучить плюсы за 21 день»

Отличная статья! Хороший обзор. В очередной раз убеждаюсь, что я не хочу писать на С++. Такого сборища костылей я не видел ни в одном языке. Вспоминаются с ностальгией языка г-на Вирта — все лаконично и сам язык тебе подсказывает верный путь, как нужно оформить мысль. Без вот этих вот глубинных подсмыслов.

Пример про макросы: assert(Widget(2,3)) выполняется, а assert(Widget{2,3}) ломает препроцессор. Дело в том, что у макросов есть специальное правило, которое правильно читает запятую внутри круглых скобок, но оно

Честно говоря, макросы и с шаблонами надо дружить с помощью известного костыля. Полагаю этот костыль необходим и для инициализацией списком.
#define SAB(...) __VA_ARGS__
...
    assert(SAB(Widget{2,3}))
...
    CPPUNIT_TEST(SAB(test<Widget, Thingy>))
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Информация

Дата основания
Местоположение
Россия
Сайт
jugru.org
Численность
51–100 человек
Дата регистрации

Блог на Хабре