Comments 125
Неудержался
В С не нужно соблюдать порядок элементов, то есть в нашем примере можно сначала инициализировать с, а потом а. В С++ так делать нельзя, поскольку вещи конструируются в порядке, в котором они объявлены.
Который день сижу и плАчу.
Вот тоже не понимаю: что мешало? Ведь в конструкторах разрешили использовать любой порядок написания, что не влияет на порядок инициализации. А здесь вдруг порядок написания менять ни-ни.
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, вроде как, позволяет переставлять аргументы… лучше этого не делать.
Ну тут шляпа наполовину — пропускать элементы можно, но вот порядок сохранять придётся.Чего, собственно, и достаточно. Да, немного неудобно — но на читабельность не влияет, а при написании… компилятор вам поможет.
Мы не должны мириться с тем, что комитет загнал в угол сам себя.
По сути: кому помогал тот факт, что поля класса инициализируются в порядке их объявления в классе? Это массовый источник заблуждений. Все ожидают, что они инициализированы так, как ты записал в списке инициализации конструктора, и мне до сих пор приходится себя одергивать, когда я его пишу. То, что порядок инициализации не перегружен порядком, заданным мной к конструкторе, меня удивляет. Можно себя очень глубоко загнать — 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?Зависит от прикладного кода, в общем случае. Так-то и код на C90 — формально кодом на C++17 является.
Если же рассматривать «идеоматичный C++17»… то это его основа. Поверх этого надстроены RAII, умные указатели и прочее, прочее, прочее. Так называемый «современный C++» чуть менее, чем полностью поверх этой концепции построен. Но да, это немного в расширительном смысле, с выходом за границы одного объекта.
По мне так это скорее исключение, когда у вас эти поля ходят как-то друг к другу изнутри через ссылки или указатели друг на друга, и вам нужно отконтролить их жизненный цикл в совокупности — развязать направленный граф, так сказать. Часто ли?А часто ли вас вообще волнует в какой последовательности подъобъекты конструируются? В моей практике ситуация такая:
1. В подавляющем большинстве случаев мне вообще пофигу в какой последовательности подобъекты конструируются и уничтожаются.
2. Зато уже если так случилось, что мне это не всё равно… то почти всегда мне при этом нужно и чтобы и удалялись они тоже в предсказуемой последовательности.
Я понимаю, что RAII, copy elision и пр. полагаются на фундаментальное свойство. Я не могу ответить, как. Я хотел бы иметь этот ответ, чтобы объяснить изучающим С++: «это так, ПОТОМУ ЧТО вот», а не просто «потому что». Пока не могу. Может быть, поможете :)
Конкретно, вот например RAII. Почему ИМЕННО ему важен детерминированный порядок инициализации и разрушения?
Хочется показать 2-3 примерами, что фундаментальное свойство — так его и назовём — не вещь в себе, а требование корневых механизмов С++.
#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».
Я совершенно понимаю, что они хотели ей сказать — что, мол, типа, раз в куске кода переменные у нас создаются на стеке в порядке их появления в коде, то пусть так будет и в scope класса, круто же, единообразно? Но, нет, не круто. Люди не смотрят на класс как на линейный код, потому что класс — не линейный код. Это НЕУПОРЯДОЧЕННЫЙ набор полей и методов.
Первое со вторым, как бы, не очень совместимо…
Есть и языки, где в класса во время работы программы могут появляться и исчезать поля во время работы программы (см. MOP) — и именно таким был ООП изначально.
Считать ли теперь C# и Java «ущербными» из-за того, что там это невозможно сделать? Вопрос, как обычно, философский.
Класс в C++ — это упорядоченная последовательность полей и методов. И их порядок — важен. Как бы вам ни хотелось чего-то иного.
Она безусловно детерминирована, но настолько неявна, что лучше её полностью игнорировать.От умных указателей вы тоже, стало быть, отказываетесь? Там же неявно где и как они память освободят!
Это НЕУПОРЯДОЧЕННЫЙ набор полей и методов.Нет, нет и нет. Почитайте правила, хотя бы:
You cannot…
For virtual member functions
change the order of virtual functions in the class declaration.
А уж про добавление/удаление полей я вообще молчу: поля (даже приватные) — это часть интерфейса класса. И, разумеется, их порядок важен.
Да, в C++ есть вот такие вот, несколько странные и неожиданные «правила игры» — но это плата за эффективность.
Какой смысл платить за них, но не использовать? Для этого другие языки есть…
От умных указателей вы тоже, стало быть, отказываетесь? Там же неявно где и как они память освободят!А почему надо отказываться? .reset( в нужном порядке ему, счётчик ссылок далее позаботится об удалении. В том примере выше я тоже ничего не удалял delete-ом, хотя вызов reset этому эквивалентен, но порядок удаления указал совершенно жёстко. Или вы о порядке для класса, содержащегося в shared_ptr-объекте?
И да, можно объединить недостатки C++ и C явно вызывая эту операцию с помощью
.reset(nullptr)
— вот только зачем в этом случае вообще нужны умные указатели — неясно совершенно.Вообще неясно зачем при использовать язык, основным и главным достоинством которого являются неявные операции (неявное преобразование типов, неявный вызов конструктора и деструктора и так далее) и носиться с лозунком «явное лучше неявного».
Хотите всего явного — есть же C, чёрт побери!
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. И куда вы это в своём «прувере» засовывать собрались?Всё дело в выравнивании полей и особенностях невыровненного доступа на различных архитектурах.
В любом случае — это вещь, которую ни компилятор, ни формальный «прувер» не в состоянии правильно запроектировать. Потому что всё зависит от структуры вашей программы. Причём не локальной, а глобальной.
И никто, кроме вас не будет знать что для вас полезнее — увеличить размер структуры, но добиться того, чтобы данные лучше ложились в кеш-линии, либо уменьшить — и добиться, чтобы страктуры не вытесняли другие данные из L1.
Дык, выравнивание по размеру кэш-линии.
Но мне кажется, что ситуаций, где это будет оказывать реальное влияние на производительность, будет очень и очень мало. Плюс будет жёсткая привязка к конкретному процессору.
Конкретно в моём случае возня с кэшем играла роль только при эффективной реализации математических алгоритмов.
sizeof(T) == 64
не гарантирует помещения всего значения в одну кешлинию.
Там размер как раз не 64 ни в одном из вариантов. И именно потому, что там не написано alignas(64)
— большинство маллоков разместят эту структуру так, что, противоестественным образом, версия со string
вначале будет быстрее, если мы будем часто обращаться к координатам… и наоборот, она будет медленнее, если мы будем чаще отращаться к строкам.И да — всё это дико патологический случай. В большинстве случаев такая тонкая оптимизация никому особо не нужна — я просто хотел показать, что у компилятора нет никакой, ну вот совсем никакой возможности всё и всегда сделать «оптимально». Это от программы зависит.
Значит ли это, что нельзя сделать лучше, чем в C++? Нет, не значит, в Rust сделано лучше: обычно компилятор подбирает layout структур сам, «способом, приближенным к оптимальному», но если очень надо — layout можно задать самому, как в C/C++.
Нужно ли это фичу тащить в C++? Не уверен: практически в 99% случаев заметной разницы между «тупым» варинтов, когда вы размещаете поля так, как вам удобнее о них думать и «способом, приближенным к оптимальному», как в rust — не будет, а там где это окажется важно — компилятор, скорее всего, не очень-то и справится.
Но нужно больше наблюдений за практическими проектами на Rust.
Ну это всё равно как обсудлать с человеком, чинящим телевизоры путём сбрасывания их с обрыва (в надежде, что детали встанут на место и телевизор заработает) наилучший вид обрыва для этого: крутой там или не очень… вообще-то телевизоры не так чинятся… и программы не так оптимизируются.
Об оптимизации чего вообще речь?Правильные вопрос.
Доступа к полям?Ну в первом случае это очевидно: раз у нас два независимых мьютекса, то большой шанс, что они будут браться независимо. И, стало быть, нахождение их в одной кеш-линии — крайне нежелательно. Хотя можно себе представить и патологическую программу, где всё наоборот.
Аллокации пустой структуры?Тоже варинт. Странный, но возможный.
Ее заполнения?(если это пункт, то как заполняется строка?)Это может быть как строка с сообщением об ошибке (в большинстве случае пустая), так и ключ поиска (к которому обращаются чаще всего).
Да, вы правы — без знания того, что, кто, когда и как делает с этой структурой не только бенчмарки бессмысленны, но и вообще — неясно под какой паттерн доступа к памяти мы всё это оптимизировать хотим.
И если бы были заданы эти вопросы — претензий бы не было. Но ведь от меня просят объяснить не это. А просто показать бенч. Ну состряпаю я его — дальше что? Что и кому это покажет?
И вообще, по-хорошему, переупорядочить поля согласно паттерну доступа, кто там первый загрузится в кэши с кодом и т.д. — это задачка для 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ый! И всё, проблема решится.
Собственно, я хотел явности в порядке, оно теперь у меня есть.Ну да. А ещё у вас есть возможность напутать в вызовах. И не вызвать в нужном месте
release
. Ну и? Чего вы этим, собственно, добиваетесь?Да, здесь я, безусловно, борюсь с языкомА зачем?
ожидая такой защитой, что кто-то о нём точно забудет.Это вряд ли. То есть заставить кого-то выкинуть ваш код и написать нормальный, читабельный, наверное кого-то вы и сможете побудить (хотя зависит от того, насколько этот ужас его «достанет»). А вот забыть о том, что C++, всё-таки, должен выглядеть по другому… это вряд ли.
Что ж поделать, раз так задизайнили язык.Выбрать другой язык? С тем же успехом можно сказать, что в английском и русском глагол «не там» стоит — и начать разговаривать в стиле Йоды. Ничего, кроме смеха вы, таким образом, не добъётесь.
Более того, я считаю, что именно этим правилом они и порочат 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, компилятору на это по боку, и язык ничего не требует. Бред же.Почему бред? Не вижу ничего странного в том, что метка текстового поля будет знать о своём текстовом поле. Просто она не должна будет, в этом случае, пытаться обращаться к нему в конструкторе и деструкторе — но само-по-себе знание вполне законно.
Вы защищаете плохой дизайн, я считаю.Я не «защищаю» «плохой» дизайн. Я объясняю почему он таков, каков он есть.
Хотите другого дизайна? Вперёд! Языков в мире — тысячи, если не миллионы. Можете и свой создать.
Но делать вид, что язык устроен не так, как он устроен и «бороться» с ним… ну что за детский сад, ей богу… назло бабушке отморожу уши…
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;
}
Это комитет расстреливает нам ноги.
P.S. Вы занимаетесь манипуляциями. Это все равно, что назвать «проблемным» код типа такого:
int foo()
{
int f = 1;
return f;
}
а когда вам справедливо укажут, что код корректный, прислать ссылку на годболт с «немного измененной» версией:
int &foo()
{
int f = 1;
return f;
}
и вы такой: «ага-ага, видите-видите, упало!» :)
В вашем примере выше никаких проблем не было. Ну вот совсем никаких. А вашем примере, который упал — были. При этом вы упорно делаете вид, что это — эквивалентные примеры.
Вывод: вы либо не знаете C++, либо троллите. Судя по остальным комментариям — я склоняюсь ко второму.
struct Data {};
struct Host {
Data& d;
Host(Data& d) : d{d} {}
};
// this WILL compile ;(
struct Mixed {
Host h{d};
Data d;
};
Она компилируется, но может привести к неочевидным последствиям в будущем (при развитии кода классов). Это было показано в последующих примерах.
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 };
};
А вы своим предложением разруливать автоматически порядок инициализации всё сломаете, корректный код просто перестанет компилироваться — нехорошо…
Хочется показать 2-3 примерами, что фундаментальное свойство — так его и назовём — не вещь в себе, а требование корневых механизмов С++.Вы путаете причину и следствие. Нет никаких физических законов, заставляющих проектировать C++ так, чтобы деструкторы вызывались строго в обратном порядке. Но если мы так сделали и так спроектировали язык — то дальше к нему все привыкают и на него навешивается масса конструкций.
Примеры вам привели. И да, пример, который вы придумали сами (когда объекты «живут» внутри другого объекта, при этом имеют так же ссылки и друг на друга) — тоже не редкость. Скажем метка и поле ввода, помеченное этой меткой. Не так важно — кто из них создаётся и удаляется первым… но после того, как вы об этом договоритесь (допустим первым удаляется поле ввода) то вам потребуется, чтобы это правило никогда не нарушалось (так как, напрмер, поле ввода будет «иметь право» обращаться к метке).
Пришельцам из C#/Java это тяжело понять, но код в конструкторе в C++ — отнюдь не является редкостью (в отличие от финалайзеров в C#/Java)… и это происходит именно потому, что язык даёт гарантии на тему того, когда они вызовутся.
А вот наблюдать за адекватным развитием некоторых языков доводится с завистью. Хочется все 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.
Но так как я сам себе заказчик, а замены С++ по эффективности нет, вся эта мышиная возня мне сооовсем не на руку.А почему «замены С++ по эффективности нет» — никогда не задумывались? Или считаете, что оно само так, случайно, произошло?
Зачем вручную делать работу компилятора?Потому что на примере конструкторов уже убедились, что когда компилятор за человека делает эту работу — человек оказывается недоволен.
Вы уверены, что теперь имена аргументов не протекут в ABI?Ну как бы довольно очевидно, что они проткут в ABI, иначе как вызывать простейшую функцию с двумя аргументами.
Всё это, в принципе, разрешимо — но очень сильно меняет весь язык и совершенно неясно: насколько это реально облегчает жизнь.
В следующем смысле: да, я знаю что работать с API типа Win32 API, где у каждой функции по 10 аргументов, многие их которых — это структуры с кучей полей… неудобно и сложно… но может быть вместо кардинальных изменений языка лучше — не создавать таких API без крайней необходимости?
Только вряд ли это кому-нибудь нужно: структуры в 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)
{}
Примеры вам привели. И да, пример, который вы придумали сами (когда объекты «живут» внутри другого объекта, при этом имеют так же ссылки и друг на друга) — тоже не редкость.
Кстати в расте так нельзя сделать совсем, и я думаю в том числе потому что нету этого провалила про порядок полей в структуре
А если дано 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;
}
В разделе про назначенную инициализацию (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);
//....
}
Очень удобный синтаксис на мой взгляд и вполне мог бы пригодиться и в С++. Кстати, совсем не зависит от порядка элементов в квадратных скобочках.
По крайней мере clang.
Кстати интересно почему его не спешат в кресты добавлять, боятся усложнять парсер из-за неоднозначности с синтаксисом лямбд?
int i4 = int(); //inits with 42
А разве не нулем?
int i14(7, 9); //compile-time error int i17(7, 9); //compile-time error
Не говоря уже о том что мне вообще непонятно как можно некомпилирующийся код посчитать за «способ инициализации переменных».
Если погромист ответственный пацан, то он пальчиками должен проинициализировать каждую переменную, обьект etc. Ибо при переходе на новую версию компилятора/компьютера/операционной системы возможно int i при первом обращении будет не нуль и даже не 255 в восьмибитных или 2^64 в соответствующих системах. И тогда шагай step-by-step ищи, где собака порылась. И не надеяться на порядочность компиляторописателей. Они тоже люди. Просто выносим инициализацию в include и все дела. Иначе старый добрый С++ превратится в Васик с непредсказуемыми результатами runtime. Я так думаю(с)(Фрунзик М.)
Попытка вот так вот «удушить всё» в краткосрочной перспективе ничего не улучшает, а в долгосрочной перспективе ухудшает: если таких умных, как вы, оказыватся много, то компилятор учат ваш код игнорировать, в этом игрировании обнаруживаются ошибки… ну и дальше — классика, гонка вооружений…
Но тем не менее, явное лучше не явного.
Да на самом деле бесит что примитивные типы ведут себя не так как нормальные классы. Почему конструктор по умолчанию не определен у int?
unsigned bytesRead; // тут не нужна инициализация нулём
ReadFile(hFile, buffer, len, &bytesRead, NULL);
а не лучше
unsigned bytesRead = ReadFile(hFile, buffer, len);
? Передача выходных значений через аргументы это же ближе к си чем к наворотам современных плюсов. Даже иногда предлагают использовать анонимные лямбды для этого дела. И что то мне кажется что после компиляции код будет такой-же.
std::optional как вариант или std::tie. Я не знаю, сам писал (немного) раньше в си стиле. Интересно как принято сейчас делать в плюсах, разве не затем придумали исключения и все остальные штуки поновее?
Но как вам ситуация, когда в C++22 ввели наконец принудительную инициализацию всех простых типов и после перекомпиляции во всех старых программах, написанных ещё в C99-стиле, просела производительность?
Но если необходимо просто вызвать конструктор, то лично я предпочитаю использовать прямую инициализацию, то есть классический синтаксис. Я прекрасно понимаю, что в этом со мной многие не согласятся — Николай говорил, что предпочитает для этого использовать фигурные скобки. Мне кажется, что круглые скобки более очевидны, поскольку тогда синтаксис такой же, как при вызове функции, и сразу ясно, что выполняется разрешение перегрузки. Все правила здесь очевидны, не надо думать, есть тут или нет initializer_list. Мне этот подход кажется более простым и ясным: когда смотришь на такой код, сразу ясно, что он делает.
В обычном случае можно использовать то что нравится, но при использовании круглых скобок следует помнить, что порядок вычисления аргументов внутри не гарантирован.
Для фигурных же он определён слева направо.
А есть что-то такое, но про указатели и выделение памяти? Много раз слышал, что использовать сырые указатели и new не стоит, но что сейчас правильно использовать?
…
Теперь у нас есть 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>))
Не раскрыты варианты с оператором new, в том плане, что можно написать `new Foo()` а можно без скобочек `new Foo`
Использование auto везде превращает код в нечитаемый. Суть auto - упростить, а не усложнить чтение кода (например нужен в итераторах). Код чаще читается, чем пишется
> {Использование auto везде} {превращает код в нечитаемый}.
> {Использование auto} {везде превращает код в нечитаемый}.
В первой версии оно было только одно, удивительно, как минусов не наставили ;\
Я бы тут поставил тире:
> Использование auto везде — превращает код в нечитаемый.
Или — переставил бы слова, или заменил на «повсеместное использование»…
auto i = 3; // int
auto i(3); // int
auto i{3}; // в С++14 — int, но работает только для списка из одного элемента
auto i = {3}; // так и осталось std::initializer_list<int>
то ваши претензии непонятны. Это же не пример, как писать код в продакшен, а исследование, как компилятор выводит типы.Решил я значит изучить c++, на этой строке я упал в обморок
Про инициализацию уже рассказывал Николай Йоссутис. В его докладе был слайд, на котором перечислялись 19 различных способов инициализировать int:
Очевидно, что все эти способы подпадают в категорию: не плохо бы знать (особенно, если это основной язык программирования), однако применять их все не обязательно.
Тут тонкость в том, что язык бежит вперёд, не стоит на месте. И старается сохранить совместимость со старым кодом. Поэтому иногда это вырождается в такой "цирк".
Инициализация в современном C++