Потому что пример в той статье полностью корректен, и автор статьи абсолютно прав. Вот этот пример:
// Хорошо.
struct person {
person(std::string first_name, std::string last_name)
: first_name{std::move(first_name)} // верно
, last_name{std::move(last_name)} // std::move здесь СУЩЕСТВЕНЕН!
{}
private:
std::string first_name;
std::string last_name;
};
Такой код позволяет покрыть все (ну ладно, почти все) возможные варианты использования класса:
std::string first{"abc"}, last{"def"};
person p1{first, last}; // (1) копирование обеих строк
person p2{std::move(first), last}; // !!! копирование только второй
person p2{std::move(first), std::move(last)}; // (3) нет копирования
person p3{"x", "y"}; // нет копирования
Сравните со старым методом, когда передавали по const&: он однозначно хуже, потому что исключает вариант (3):
// Плоховато.
struct person {
person(const std::string& first_name, const std::string& last_name)
: first_name{first_name}
, last_name{last_name}
{}
private:
std::string first_name;
std::string last_name;
};
std::string first{"abc"}, last{"def"};
person p1{first, last}; // будет копирование, как и хотели
// Но что если мы точно знаем, что first и last нам больше не
// понадобятся? мы не можем их в таком случае переместить
// и добиться 0 копирований! Поэтому const& хуже.
Альтернативный вариант с && также хуже, но в обратную сторону:
// Странновато.
struct person {
person(std::string&& first_name, std::string&& last_name)
: first_name{std::move(first_name)}
, last_name{std::move(last_name)}
{}
private:
std::string first_name;
std::string last_name;
};
std::string first{"abc"}, last{"def"};
person p1{std::move(first), std::move(last)}; // норм
// но вот если мы НЕ хотим перемещения, то в случае && придется хитрить:
person p2{std::string{first}, std::string{last}}; // FOOOO
Если не боитесь комбинаторного взрыва, то можете дать шанс && (но зачем? реального выигрыша в скорости не будет никакого, оптимизатор не дремлет):
// Пожалейте свою клавиатуру.
struct person {
person(std::string&& first_name, std::string&& last_name)
: first_name{std::move(first_name)}
, last_name{std::move(last_name)} {}
person(const std::string& first_name, std::string&& last_name)
: first_name{first_name}
, last_name{std::move(last_name)} {}
person(std::string&& first_name, const std::string& last_name)
: first_name{std::move(first_name)}
, last_name{last_name} {}
person(const std::string& first_name, const std::string& last_name)
: first_name{first_name}
, last_name{last_name} {}
private:
std::string first_name;
std::string last_name;
};
Или вот то же самое, только с шаблонами (но опять же, зачем?):
// Заумно в данном случае (из пушки по воробьям), хотя в других может быть норм.
struct person {
template <typename T1, typename T2>
person(T1&& first_name, T2&& last_name)
: first_name{std::forward<T1>(first_name)}
, last_name{std::forward<T2>(last_name)} {}
private:
std::string first_name;
std::string last_name;
};
Даже если у вас не std::string, а какой-то объект собственноручно написанного большущего класса, и вы хотите людей заставить перемещать его (а не копировать), то в таком случае лучше запретить конструктор копирование у этого большущего класса, нежели везде передавать его по &&. Так надежнее, да и код короче.
Напоследок пара вариантов, как делать НЕ СТОИТ:
// Кошмарно.
struct person {
person(const std::string& first_name, const std::string& last_name)
: first_name{first_name}
, last_name{last_name}
{}
private:
// НЕТ и НЕТ: это мегаопасно, никогда не сохраняйте константные
// ссылки в свойствах объекта
const std::string& first_name;
const std::string& last_name;
};
person p{"x", "y"}; // ха-ха-ха, приехали
И так тоже не надо:
// Плоховато.
struct person {
person(std::string& first_name, std::string& last_name)
: first_name{first_name}
, last_name{last_name}
{}
private:
// так можно иногда, но лучше все же воспользоваться shared_ptr:
// будет медленнее, но безопасно
std::string& first_name;
std::string& last_name;
};
Почему так происходит, каков фундаментальный принцип? Он прост: объект, как правило, должен ВЛАДЕТЬ своими свойствами.
Если объект не хочет чем-то владеть, то он может владеть shared_ptr'ом на это «что-то». Кстати, shared_ptr'ы в таком случае тоже надо передавать по значению, а не по константной ссылке — тут разницы с самым первым примером в начале статьи никакой:
// Лучше (если нет выхода).
struct person {
person(std::shared_ptr<portfolio> pf)
: pf{std::move(pf)} // std::move здесь важен для производительности
{}
private:
std::shared_ptr<portfolio> pf;
};
auto psh = std::make_shared<portfolio>("xxx", "yyy", "zzz");
...
person p1{psh};
person p2{std::move(psh)}; // (X) так эффективнее, если psh больше не нужен
Обратите внимание: std::move для shared_ptr совершенно легален, он исключает накладные расходы на блокировку счетчика ссылок shared_ptr в памяти (сотни циклов CPU) и на его инкремент. Он никак не влияет на время жизни объекта и остальные ссылки на него. Но (X) можно делать, конечно, только в случае, если ссылка psh в коде ниже нам больше не нужна.
Мораль: не используйте const& повально. Смотрите по обстоятельствам.
P.S.
Используйте {} вместо () при передаче параметров конструктора. Модно, современно, молодежно.
Комментарии (67)
ncr
22.07.2019 21:50-1Такой код позволяет покрыть все возможные варианты использования класса
Правда?myxo
22.07.2019 23:36+1Ну так все правильно.
string_view — это специальный класс, который не владеет данными. Если вы хотите сделать копию данных и передать её в класс, это нужно делать явно на стороне вызывающего.ncr
23.07.2019 00:08вы хотите сделать копию данных и передать её в класс, это нужно делать явно на стороне вызывающего
Я? Нет, лично я не хочу делать копию.
Если class person хочет у себя внутри иметь копию строк — это его право и, в общем случае, детали реализации.
Сваливая бремя конструирования этих строк на клиента, вы потенциально загрязняете ему код:
std::string_view First = ..., Last = ...; person Person{std::string{First}, std::string{Last}};
— Красота же, да?
Принимайте string_view, он неявно и практически бесплатно конструируется из всего, и копируйте его потом себе как хотите (если надо).
Единственный «недостаток» — да, нельзя переместить rvalue-строку внутрь объекта (и этим сэкономить аж одно копирование). Если у вас есть сомнения в эффективности — сначала профилируйте, и только если вы действительно можете что-то выиграть на этих временных объектах — оптимизируйте.DmitryKoterov Автор
23.07.2019 03:18(Риторический вопрос.) В этом примере по ссылке, которую вы привели:
class D { // Good string s1; public: A(string_view v) : s1{v} { } // GOOD: directly construct };
… могу ли я замувать существующую строку внутрь D так, чтобы было вообще ровно ноль копирований?
std::string s{"abc"}; // взята откуда-то и потом больше не нужна D d{std::move(s)}; // orly?
А теперь представим, что там не firstname/lastname, а какие-до данные произвольной длины. Или же объект произвольной структуры (общий случай).
twinklede
23.07.2019 03:39Единственный «недостаток» — да
sv — не си-строка, но прозрачно создаётся из си-строки со всеми вытекающими.
Если у вас есть сомнения в эффективности — сначала профилируйте, и только если вы действительно можете что-то выиграть на этих временных объектах — оптимизируйте.
Никакой профайлер ничего не покажет. Подобные советы говорят лишь о крайне странных представлениях о профайлере и отсутствии опыта работы с ним(кроме как в примитивных случаях).
Хорошо. Мы имеет оверхед на передаче. У нас тысячи функций, передаются десятки, сотни разных типов объектов. Всё это будет размазано и десяткам/сотням разных функций. Как мы вообще поймём, что проблема есть и как мы её локализуем? Да никак.
Даже если мы увидим много memcpy/malloc, то это не всегда нам позволит найти и локализовать причину. Ну узнаем мы, что из вызывают эти десятки/сотни функции(вместе с её тысячами других). Какие из этих конструкторов лишние?
Поэтому, никакой профайлер тут почти никогда не поможет. И проще изначально определиться с правильным подходом и его использовать.khim
23.07.2019 05:16Подобные советы говорят лишь о крайне странных представлениях о профайлере и отсутствии опыта работы с ним(кроме как в примитивных случаях).
Не обязательно. Гораздо чаще они свидетельствуют о том, что человек никогда не пытался сравнивать свой код с кодом, который писался людьми, которые реально думают о производительности.
Я с этим феноменом сталкивался много раз: люди считают, что если они могут ускорить код раза в 3-4 с помощью профайлера — то это указывает на то, что они всё сделали «правильно». Тот факт, что можно сделать «неправильно», так что профайлер применить куда-либо будет очень сложно, но всё будет работать ещё раз так в 5-6 быстрее… им обычно в голову не приходит.
Всё это довольно-таки грустно и напоминает попытку поставить на автомобиль Формулы-1 автоматическую коробку передач и шипованную резину.
Если вас устраивает вариант, когда код работает в 3-5 раз медленнее, чем он мог бы работать… и вы готовы с подобным замедлением мириться ради «красоты кода»… то зачем вы вообще связались с C++? C# или Java дадут вам желаемое с куда меньшими затратами!ncr
23.07.2019 15:02людьми, которые реально думают о производительности
«У меня естьмолотокC++ и теперь всё вокруггвоздинуждается в оптимизации».
Люди, верящие, что они и без профайлера знают, что надо делать, могут себя не жалеть, ородинепроизводительности думать, и неделю ковырять конструктор, чтобы идеально передавать строку во всех возможных вариантах.
Закончив в пятницу и убедившись, что они ускорили код в 10 (допустим) раз они идут на хабр делиться мудростью и предвкушать премию, а тем временем выясняется, что:
— 10 раз — это 1 мкс вместо 10 мкс;
— этот идеально оптимизированный код вызывается исключительно в контексте чтения из / записи в БД, выполняющемся на 3 порядка дольше, и теперь работа с БД занимает не 5 с, а 5 с;
— либо этот код вызывается исключительно когда пользователь заполняет форму, и заполнение формы теперь занимает не от 2 минут до 3 часов, а от 2 минут до 3 часов;
— либо этот код вызывается 1-2 раза за всё время работы, и теперь программа майнит монету не неделю, а неделю;
— либо этот код в 99 случаях из ста работает с джонами смитами, элтонами джонами и риками санчезами, и только в одном случае с даздрапермой череззаборногузадерищенской, так что SSO превращает все ручные оптимизации в тыкву;
— либо этот класс всегда живет меньше, чем его агрументы, поэтому можно было ничего не копировать и хранить указатели / ссылки;
— и т.д. и т.п.
А менеджеру такого гуру оптимизации теперь надо как-то объяснить клиенту, на что потрачена неделя и за что тот заплатил условный килобакс денег.
Ну и решить, что дальше делать с гурой.
«Premature optimization is the root of all evil». ©
Замеряйте.khim
23.07.2019 18:01«Premature optimization is the root of all evil». ©
Да легко. Вспоминаем вторую фразу из той же самой статьи Кнута:
Замеряйте.In established engineering disciplines a 12% improvement, easily obtained, is never considered marginal; and I believe the same viewpoint should prevail in software engineering.
А вот теперь — можно и заменять. И да — разумеется мерить нужно не скорость передачи аргументов в конструктор.
«У меня есть
Не совсем так. C++ — это не один молоток. Это большая коллекция молотков. Самых разнообразных — но неизменно сложных и опасных.молотокC++ и теперь всё вокруггвоздинуждается в оптимизации».
Однако если мы начинаем обсуждать вопрос «а нам, так-то, гвозди забивать и не нужно» — то это значит, что мы неправильно выбрали ящик с инструментами. Изначально.
Потому что в большой коллекции C++ ничего, кроме молотков и нету. Иными словами: если вас устраивает код, в 3-5-10 медленнее, чем оптимальный — то вам не нужно использовать C++ вообще.
Не используйте «ящик с молотками» потому что он модный популярный, возьмите C#, Java (а то и Python/Mathlab какой-нибудь) — и не нужно будет произносить идиотских мантр про «premature optimizations».
Antervis
24.07.2019 21:51«Premature optimization is the root of all evil». ©
Допустим, я знаю, что std::move «вот здесь»точнона любом нынешнем компиляторе сэкономит мне аллокацию/деаллокацию/копирование, и никогда не сделает хуже, почему бы мне этот мув не написать? Зачем мне доводить код до того состояния, когда его надо оптимизировать через профилировщик, указывающий на 40% нагрузку от memcpy (true story)? Тем более что многие такие оптимизациями даются «невероятным трудом» из категории «дописать unordered_». Я бы даже не назвал это преждевременной оптимизацией, скорее, предотвращением деградации.
DustCn
23.07.2019 16:01Подход профилируй-оптимизируй он правильнее чем вы начнете оптимизировать основываясь только на своих каких то предположениях. Не все профилировщики работают на уровне функций. Тот же Втюн вполне может спуститься на уровень базовых блоков (basic block) и показать потерю времени в куске, связанном с конструктором.
>>Даже если мы увидим много memcpy/malloc
Уже будет хорошим поинтом чтобы насторожиться. Дальше в том же профилировщике открывается статистика по call site.khim
23.07.2019 18:17Тот же Втюн вполне может спуститься на уровень базовых блоков (basic block) и показать потерю времени в куске, связанном с конструктором.
Однако ни один VTune не может рассказать вам как избежать кеш-промахов… а это — самое важное, что есть в оптимизации вообще. Один промах с необходимостью похода в память — эквивалентен сотням операций и даже L2/L3 даёт не такой большой выигрыш, чтобы этим можно было пренебречь.
L1 же имеет размер, во-первых крошечный, а во-вторых — фактически не меняющийся со временем: Zen2 имеет кеш в 32Kb, то есть примерно столько же, сколько было в кеше PowerPC 601 четверть века назад!
Уже будет хорошим поинтом чтобы насторожиться. Дальше в том же профилировщике открывается статистика по call site.
Ну обнаружили мы, что это произошло из-за очень SOLIDной архитектуры, которая «размазала» десяток счётчиков (которые нам реально нужны для реализации алгоритма) по множеству разнообразных, хитро связанных между собой структур данных, коих у нас насчитывается порядка полутысячи (реальный пример из реальной программы). Ваши дальнейшие действия?DustCn
23.07.2019 21:28VTune вполне может показать вам кэш- и тлб-миссы.
И не забывайте про аппаратный префетч, к которому можно добавить и программный.
>«размазала» десяток счётчиков (которые нам реально нужны для реализации алгоритма) по множеству разнообразных, хитро связанных между собой структур данных, коих у нас насчитывается порядка полутысячи…
Структуры не исполняются. Оптимизировать надо в первую очередь бутылочные горлышки, а не лазить по зиллиону сеттеров-геттеров.
Если у вас в приложении конструктор занимает 80% времени значит вы изначально что-то не то делаете.
khim
24.07.2019 11:29Структуры не исполняются.
Исполняется код, «ползающий» по этим структурам.
И не забывайте про аппаратный префетч, к которому можно добавить и программный.
Но никакой префетч, ни программный, ни аппаратный не сделает прохождение по 3-5-10 уровням индирекции близким по скорости к обращению к локальной переменной или, ещё лучше, к регистру.
Оптимизировать надо в первую очередь бутылочные горлышки, а не лазить по зиллиону сеттеров-геттеров.
Это если вам нужна занятость и вы хотите в течение 10 лет каждый месяц отчитываться об ускорении на 5-10-15% (вначале больле, потом меньше).
Если же вы хотите сделать быстрый код — то заниматься нужно совсем другим. Единственные, кому действительно полезно смотреть на VTune — это разработчики рантаймов. Так как у них код исполняет алгоритмы, созданные другими и, соответственно, на алгоритмы они повлиять никак не могут.
Если у вас в приложении конструктор занимает 80% времени значит вы изначально что-то не то делаете.
Совершенно не обязательно. Если у вас 80% времени уходит на конструктор вспомогательного объекта, который вам, в сущности, не нужен — то это одно. А если это — часть «внутреннего цикла», делающего основную работу — то совсем другое.
Гляньте как-нибудь на профайл «вылизанных до упора» энкодеров видео. 80% «на конструктор» вы там, конечно, не увидите — но там будут весьма и весьма «горячие»' участки. Где будет как раз, суммарно, 80% времени проходить.
Вот так и выглядит оптимальная программа. А «ровный» спектр, где все «горячие участки» потушены — это как раз результат многолетнего бездумного применения VTune. Такая программу, обычно, можно ускорить в несколько раз — если подумать.DustCn
24.07.2019 13:12Это если вам нужна занятость и вы хотите в течение 10 лет каждый месяц отчитываться об ускорении на 5-10-15% (вначале больле, потом меньше).
Ну лазьте по ним и отчитывайтесь, я не понимаю вы спорите ради спора?
Если же вы хотите сделать быстрый код — то заниматься нужно совсем другим.
Сделайте сначала рабочий. Отладьте. Оптимизация прототипов и нерабочих, ошибочных участков хороший повод просто погреть атмосферу.
Совершенно не обязательно. Если у вас 80% времени уходит на конструктор вспомогательного объекта, который вам, в сущности, не нужен — то это одно. А если это — часть «внутреннего цикла», делающего основную работу — то совсем другое.
Совершенно обязательно. Программа должна считать данные, выполнять полезную нагрузку, а если она занимается «инкапсуляцией» 80% времени, это говорит о богатом внутреннем мире программиста и что неправильно был выбран тип данных.
Гляньте как-нибудь на профайл «вылизанных до упора» энкодеров видео. 80% «на конструктор» вы там, конечно, не увидите — но там будут весьма и весьма «горячие»' участки. Где будет как раз, суммарно, 80% времени проходить.
Глядел лет 15 назад, потом ушел в HPC. К чему это замечание — мне не очень понятно. Все оптимизаторы это 80% и ковыряют, забив на остальные 20.
А «ровный» спектр, где все «горячие участки» потушены — это как раз результат многолетнего бездумного применения VTune.
С какого бодуна у вас родилась такая мысль — я не знаю. Я могу прнести вам пару тройку программ, не нюхавших Втюна с таким же профилем. Бездумное применение Втюна — это оксиморон. Не нравится вам инструмент — нечего других упрекать в его использовании.
encyclopedist
23.07.2019 00:14Я попытался сделать сводную таблицу эффективности разных способов передачи
twinklede
23.07.2019 03:09-2Потому что пример в той статье полностью корректен, и автор статьи абсолютно прав.
Нет, очевидно. Это просто самый наивный способ передачи. Так напишет любой, кто 1-2 раза видел С++, а автор его таки 1-2 раза и видел.
копирование обеих строк
Копирование есть, оно есть всегда.
Если не боитесь комбинаторного взрыва, то можете дать шанс && (но зачем? реального выигрыша в скорости не будет никакого, оптимизатор не дремлет):
Подобная аргументация ничего не стоит.
Очевидно, что это не одно и тоже. В случае с передачей по ссылке не будет копирования, когда как в случае с первым вариантов оно будет. Ещё на 20-40 байтовой строке можно что-то говорить о том, что копирование бесплатно, но объекты не всегда 20-40байт.
Даже если у вас не std::string, а какой-то объект собственноручно написанного большущего класса, и вы хотите людей заставить перемещать его (а не копировать), то в таком случае лучше запретить конструктор копирование у этого большущего класса, нежели везде передавать его по &&. Так надежнее, да и код короче.
Надёжнее и быстрее как раз через &&. К тому же, к чему определяется это ложное разделение? Одно не мешает другому.
(из пушки по воробьям)
И подобная тоже.
Почему так происходит, каков фундаментальный принцип? Он прост: объект, как правило, должен ВЛАДЕТЬ своими свойствами.
Что это за принцип такой и с чего он должен кого-то волновать? Всё это догматическое raii достало уже всех — оно показало свою полную несостоятельность. Ещё с C++11 от этого глупого догмата начали отходить.
Если объект не хочет чем-то владеть, то он может владеть shared_ptr'ом на это «что-то».
Зачем захламлять код этим мусором, когда есть нормальные ссылки и сторедж может быть не только в хипе. Это опять какой-то догматизм.
pf{std::move(pf)} // std::move здесь важен для производительности
Мы копируем что-бы потом замувать, хотя можно с тем же успехом передать по ссылке, но. Что мы этим получим.
1) Мы получим меньше оверхеда т.к. ненужно будет копировать данные.
2) Если так произошло, что что-то кинуло исключение до move sp, то мы получим те самые:
накладные расходы на блокировку счетчика ссылок shared_ptr в памяти (сотни циклов CPU) и на его инкремент.
Потому что в случае с ссылкой оверхеда на передачу не будет(он будет при копировании в поле), а в ситуации со значением будет.
3) ссылка более универсальна. Т.к. мы не всегда хотим копию объекта. Мы можем захотеть взять подстроку, либо просто использовать данные для какой-то операции внутри конструктора(не копируя/перемещая их в поле).
Мораль: не используйте const& повально. Смотрите по обстоятельствам.
Опять какое-то ложное разделение. Передача не ограничивается этими двумя вариантами.
В конечном итоге подобная передача сливает pf во всём. И рассказывать о её какой-то эффективности — глупость и неправда.
Можно говорить о какой-то простоте для начинающих, но опять же слишком много но. Даже const & куда более универсален и лучше начинающим использовать его.
Ненужно пытаться вести какие-то войны руководствуясь хейтом const & времён С++11. Нужно понимать почему и за что хейтили const &. const & не позволяет мувать временные объекты и для решения этой проблемы существует pf. Решение с «по значению» просто более понятное людям и в какой-то мере может служить заменой pf, но эта замена не полноценна.
Но, людям свойственно использовать некий общий паттер при передаче. И с учётом того, что в 80% случаев люди не пытаются сохранить переданные объекты — переда по значению не может являться универсальной, ведь люди её начнут использовать для этого. И подобные советы — вредные советы.
Единственным правильным и универсальным решением является только «универсальная ссылка» и pf на базе неё. Всё остальное — не является полноценным и эффективным. Даже в таком самом лучшем для «по значению» кейсе.
DmitryKoterov Автор
23.07.2019 03:10+1Напишите статью-опровержение. Только сначала перечитайте все примеры в этой, пожалуйста, я там немного добавил про std::forward в том числе.
twinklede
23.07.2019 03:59-2Напишите статью-опровержение.
Мне лень.
Только сначала перечитайте все примеры в этой, пожалуйста, я там немного добавил про std::forward в том числе.
Я прочитал и именно про forward я и говорил. Я нигде не отрицал, что вы показывали pf. Я говорил о том, что ваши выводы/сравнения pf и «по значению» неправильные. Так же, я объяснял почему.
Так же, я рассказал и о том, откуда взялся этот срыв покровов с «по-значению» с которым носятся неофиты. Они продолжают повторять одно и тоже уже сколько лет. Но проблема в том, что те кто изначально об этом рассказывал — объясняли всё. Но люди выдирают оттуда какие-то куски, ничего не понимания и неся эти откровения в массы.
«по-значению» + move достаточно хайповая вещь, которая никогда не претендовала на замену pf. Так же, было чётко сказано о её применении.
как правильно передавать аргументы в конструктор или сеттер.
Про это я так же говорил. const & имеет только одну проблему — нельзя мувать. Мы можем передать временный объект, но не можем его замувать(т.к. мы не знаем — какой там объект).
Именно поэтому «по-значению» имеет смысла только тогда, когда мы заходим мувать. Т.е. когда мы ходим скопировать/замувать аргумент в поле. Но, хоть и пример именно такой(а он чисто случайно такой), то нигде и никак на это явно не указывается.
Так же, я не стал говорить о том, что существуют концепты. Есть/будет auto && | String && и pf уже не так сложно писать.
khim
23.07.2019 07:05Так же, я не стал говорить о том, что существуют концепты. Есть/будет auto && | String && и pf уже не так сложно писать.
Проблема не в том — сложно ли это написать. Проблема в том — стоит ли это делать.
Достали, если честно, теоретики, не знающие что реально происходит в компьютере вообще и в C++ в частности. Возьмите хотя бы чудесатый совет никогда не передавать «сырые» указатели, а всегда передавать либоunique_ptr
либо ещё какой «умный» указатель. Ну потому что скорость та же, а надёжность — таки выше… а вы уверены, что скорость — таки та же? Точно уверены? Ну так гляньте сюды и ужаснитесь.
В действительности всё совсем не так, как на самом деле. Увы. И потому слепо верить в концепты,auto&& | Sring&&
и прочее… я бы поостерёгся.
Вот выйдут эти самые концепты, поработаем с ними — и можно будет уже сказать: так оно — или не так. А пока — статья описывает далеко не самый худший подход. Уж всяко получше, чем бездумное использование pf где нужно и где не нужно будет…
P.S. Кстати, история сunique_ptr
— вот как раз типичная история с C++. Теоретически вроде как быunique_ptr
должен вести себя так же, как простой указатель в смысле эффективности… Однако же… не ведёт. Даже и близко не ведёт. И да — это, как бы, теоретически, в принципе, можно было бы исправить. И, я думаю, когда-нибудь это таки исправят. В конце-концов знаменитый бенчмарк Степанова довели, в конце-концов, до единицы, так ведь? Ну и тут — тоже, теоретически, ничего не мешает… Теоретически-то ничего не мешает, а вот практически — скоро 10 лет пройдёт, аstd::unique_ptr
— всё ещё не так эффективен, как «сырой» указатель…DmitryKoterov Автор
23.07.2019 11:17Так это… надо ж смотреть не только как функция в вакууме выглядит, а еще и как ее вызов заинлайнится. И на -O3.
В случае заинлайнивания тоже unique_ptr будет выглядеть отлично от обычного указателя? Или даже если не заинлайнится, то не сгенерирует ли компилятор две версии пролога, один когда надо занулять оригинал и один когда не надо?
Естественно что в вакууме ему надо занулить старый unique_ptr, о том и ассемблер.
khim
23.07.2019 12:18В случае заинлайнивания тоже unique_ptr будет выглядеть отлично от обычного указателя?
В случае заинлайнивания — всё будет в порядке. Но тут вроде как неглупые люди «топят» за то, как раз, чтобы в интерфейсе использовать тожеstd::unique_ptr
.
Или даже если не заинлайнится, то не сгенерирует ли компилятор две версии пролога, один когда надо занулять оригинал и один когда не надо?
Нет, не сгенерирует. Вопрос не в необходимости «занулять оригинал», а в ABI. «Мелкие» структуры все современные компиляторы передают на регистрах… за исключением случая, когда оная структура содержит нетривиальный деструтктор… как несложно догадаться — «умный» указатель без нетривиального деструктора окажется, как бы помягче, не слишком «умным».
Естественно что в вакууме ему надо занулить старый unique_ptr, о том и ассемблер.
К сожалению ему не «в вакууме» нужно это делать. А в реальном коде с реальным ABI. А в вакууме, как раз, он может делать что угодно.
encyclopedist
23.07.2019 11:19+1Возьмите хотя бы чудесатый совет никогда не передавать «сырые» указатели, а всегда передавать либо unique_ptr либо ещё какой «умный» указатель.
Никто там такого не советует. Вы намеренно исказили совет и теперь его опровергаете.
Вообще, khim, вас как будто подменили. Когда-то вы писали на забре разумные вещи, а последнее время часто несете какую-то чепуху.
khim
23.07.2019 12:36Никто там такого не советует. Вы намеренно исказили совет и теперь его опровергаете.
Вы, конечно, формально, правы. Да, там есть примечание: Sometimes older code can’t be modified because of ABI compatibility requirements or lack of resources. И там предлагается разумная альтернатива: использоватьgsl::owner
.
Но вы точно уверены, что все читающие воспримут «свеженаписанный код, под самую наираспоследнюю версию одного из самых продвинутых компиляторов» (в данном случае неважно какой из компиляторов вы считаете более продвинутым — Clang или Icc) как «older code that can’t be modified»? Гложут меня смутные сомнения в этом…
Когда-то вы писали на забре разумные вещи, а последнее время часто несете какую-то чепуху.
Ну если для вас скорость работы реального кода, скомпилированного реальными компиляторами и запущенного на реальном компьютере — это чепуха, то да, можно продолжать делать вид, что ничего, кроме C++ стандарта не существует.
withkittens
23.07.2019 04:02+1Вот здесь дядька разобрал большое количество способов передачи параметров.
Ошибки и эффективность в malloc'ах.
www.youtube.com/watch?v=PNRju6_yn3o
Сам я в С++ не бум-бум, но посмотреть было интересно.twinklede
23.07.2019 04:50Ошибки и эффективность в malloc'ах.
Состоятельность подобного сравнения сомнительна, а вернее отсутствует. Существует не только malloc, а ещё и копирование. К тому же, на тех строках которые он показывал — аллокация и так не будет — будет оверхед только на копировании.
Как я уже писал — существует множество объект тяжелее условно-бесплатных строк. Хотя даже они не так что-бы и совсем бесплатны.
www.youtube.com/watch?v=PNRju6_yn3o
Это такие же трюки. Это работает только в одном кейсе и этой истории десяток лет. Я не понимаю почему её до сих пор везде рассказываю и выдают за что-то универсальное и то, что может кого-то удивить.
В конечном итоге этот трюк сливает pf, но автор нашел вывод — это похаять pf за шаблоны. Но это не проблема pf. Это общая проблема шаблонов которые нельзя ограничить и приходится обкладываться sfinae. Но её уже решили концептами.
К тому же, это решение не универсально. Оно работает только если нам нужно копировать объекты «как есть» вовнутрь объекта. Такое далеко не всегда нужно, а вернее чаще ненужно. А когда нужны все эти кейсы тривиальны и проще просто открыть все поля и инициализировать объекты через список инициализации. Это будет и проще и нагляднее и быстрее: cust{.first = «Niko»}
svr_91
23.07.2019 10:45Тем не менее в core guidline советуют использовать константные ссылки:
isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f16-for-in-parameters-pass-cheaply-copied-types-by-value-and-others-by-reference-to-constmyxo
23.07.2019 11:06+2В этой статье другой случай. Если функции нужны данные для чтения (и функция не передает эти данные в другой поток исполнения), то да, нужно передавать по константной ссылке. Здесь же идет передача данных в класс, причем класс должен владеть этими данными.
svr_91
23.07.2019 11:29Вроде бы там вообще нету рекомендации использовать std::move в таких случаях. Хотя когда-то видел, но не могу найти.
У меня просто мысль такая, что не стоит использовать std::move вообще, кроме каких-то действительно нужных для этого случаев (unique_ptr там). Что-то вроде этого совета isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es56-write-stdmove-only-when-you-need-to-explicitly-move-an-object-to-another-scope
DmitryKoterov Автор
23.07.2019 11:21Это “for in parameters”, т.е. где семантика владения не завязана. А в статье речь про конструкторы и сеттеры. Это более частный случай.
khim
23.07.2019 12:47Тем не менее формально это противоречит ES.56, который был процитирован выше.
Но тут важно помнить, что «Core Guidelines» написаны с точки зрения кода, который затачивается под гипотетический «идеальный» компилятор C++99 (но который, по какой-то причине, вы всё-таки вынуждены писать и запускать с сегодняшними, далеко не идеальными, компиляторами).
Если вас интересует вариант, который можно использовать «здесь и сейчас» — то далеко не все Core Guidelines оказываются «одинаково полезными».
darkAlert
23.07.2019 13:13Спасибо за статью! Я пользуюсь std::move, но как то всё руки не доходили использовать это для аргументов. Были мысли, что компиляторы умнее меня и можно не заморачиваться.
klirichek
24.07.2019 10:23По поводу "а зачем?" с шаблонным вариантом — смысл таки есть. Конечная цель — инициализировать член данных (а не создавать/копировать/перемещать временные объекты такого же типа; это уже детали реализации). Если класс данных умеет создаваться не только из подобных, а из неких совершенно других параметров, то шаблонный вариант просто пробросит их прямо к конечному конструктору члена, минуя вообще любое перемещение/копирование.
khim
24.07.2019 11:57Зато шаблонный вариант напорождает кучу кода во всех единицах трансляции.
В зависимости от того, к чему вы стремитесь — это может быть и хорошо и плохо.
Но в случае, если перемещение дешёвое, а копирование более дорогое (а это, всё-таки — типичная ситуация), лучше иметь один конструктор.klirichek
24.07.2019 12:59Ну, всё же не во всех, а только там, где используется. К тому же практика показывает, что в конечном варианте код, как правило, в инлайне. Т.е. явно выраженного конструктора "верхнего" объекта в виде обособленной функции вообще нет. К тому же шаблон вовсе не обязывает использовать его для множества разных типов и случаев. Он всего лишь скрывает детали. Если итоговая функция в конечном итоге вызовется дважды или даже единожды для одного-двух типов аргументов — даже там проще инкапсулировать их в шаблоне, чем приводить подробности этих самых типов.
По поводу копирования/перемещения — простая иллюстрация:
struct train_c { int m_x = 0; train_c(int x) : m_x (x) { std::cout << "\n-CTR train_c(x) " << m_x << " " << this; } train_c(train_c&& c) : m_x(c.m_x) { c.m_x = 0; std::cout << "\n-MOVE train ctr " << m_x << " " << this << " from " << c.m_x << " " << &c;} // ... copy c-tr, copy and move assignment, etc... ~train_c() { std::cout << "\n-DTR train " << m_x << " " << this; m_x = 0;} }; struct helper_c { int pad = 0; // just to distinquish this from &m_h train_c m_h; template <typename TRAIN_C> helper_c ( TRAIN_C&& c ): m_h { std::forward<TRAIN_C> ( c ) } { std::cout << "\nHELPER_TT " << this << " from " << &c << " " << &m_h << " " << m_h.m_x; } // ... ~helper_c() { std::cout << "\n~HELPER " << this; } }; template <typename TRAIN_C> helper_c* make_helper ( TRAIN_C&& c ) { std::cout << "\n====> called make_helper with " << &c; return new helper_c ( std::forward<TRAIN_C>(c) ); } helper_c* make_helper_byval( train_c c ) { std::cout << "\n====> called make_helper_byval with " << &c; return new helper_c( std::move( c )); } TEST ( functions, trainer ) { std::cout << "\n\n==> indirect ctr"; auto fee = make_helper (11); std::cout << "\n==> made fee " << fee->m_h.m_x; delete fee; } TEST ( functions, trainer_by_val ) { std::cout << "\n\n==> indirect ctr"; auto fee = make_helper_byval( 11 ); std::cout << "\n==> made fee " << fee->m_h.m_x; delete fee; }
Запускаем. Получаем:
[ RUN ] functions.trainer ==> indirect ctr ====> called make_helper with 0x7ffcb3a44b6c -CTR train_c(x) 11 0x5623c3404e44 HELPER_TT 0x5623c3404e40 from 0x7ffcb3a44b6c 0x5623c3404e44 11 ==> made fee 11 ~HELPER 0x5623c3404e40 -DTR train 11 0x5623c3404e44 [ RUN ] functions.trainer_by_val ==> indirect ctr -CTR train_c(x) 11 0x7ffcb3a44b6c ====> called make_helper_byval with 0x7ffcb3a44b6c -MOVE train ctr 11 0x5623c3404e44 from 0 0x7ffcb3a44b6c HELPER_TT 0x5623c3404e40 from 0x7ffcb3a44b6c 0x5623c3404e44 11 -DTR train 0 0x7ffcb3a44b6c ==> made fee 11 ~HELPER 0x5623c3404e40 -DTR train 11 0x5623c3404e44
Собственно, в варианте с шаблоном видим вызов конструктора и деструктора. Всё! PF + RVO полностью избавляют и от копирования, и от перемещения. Внутрь до самой сути пробрасывается исходный int.
А в варианте с передачей по значению RVO таки не избавляет от + move ctr + dtr, покуда всё равно конструируется и перемещается временный объект.
klirichek
24.07.2019 13:33И вот ещё про "кучу кода во всех единицах"...
// head.h #include <bits/move.h> struct train_c { int m_x = 0; template<typename INT> train_c ( INT && param ) : m_x ( param ) {} }; struct helper_c { int pad = 0; train_c m_h; template<typename TRAIN_C> helper_c ( TRAIN_C && c ): m_h {std::forward<TRAIN_C> ( c )} { } }; //foo.cpp #include "head.h" int ret42 () { return 42; } //bar.cpp #include "head.h" int ret42 () { return helper_c ( 42 ).m_h.m_x; }
Смотрим выхлоп от
g++ -S foo.cpp
(где должна "напородиться куча кода"). Файл целиком:
.file "foo.cpp" .text .globl _Z5ret42v .type _Z5ret42v, @function _Z5ret42v: .LFB19: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $42, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE19: .size _Z5ret42v, .-_Z5ret42v .ident "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0" .section .note.GNU-stack,"",@progbits
Не видим вообще упоминаний; что, в общем-то, очевидно.
В случае вызова с оптимизацией О2g++ -O2 -S bar.cpp
результат аналогичен для обоих файлов (кроме имён самих файлов)
.file "bar.cpp" .text .p2align 4,,15 .globl _Z5ret42v .type _Z5ret42v, @function _Z5ret42v: .LFB19: .cfi_startproc movl $42, %eax ret .cfi_endproc .LFE19: .size _Z5ret42v, .-_Z5ret42v .ident "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0" .section .note.GNU-stack,"",@progbits
Т.е. ровно одна значимая команда (кроме ret). Что тоже достаточно очевидно.
khim
24.07.2019 14:43Всё это хорошо выглядит на игрушечных примерах. А кончается тем, что наша внутрифирменная обёртка над Git (cloud-based, всё очень хайпово, да) занимает 500 мегабайт.
И внушительный процент кода — это как раз автоматически напорождавшийся код шаблонных функций.
Как я уже сказал: иногда — в этом есть смысл. Но нужно чётко отдавать себе отчёт в том, что, где и для чего вы желаете.
Siemargl
24.07.2019 13:29+1Как глупо породивший этот весь флейм в упомянутом топике таки спрошу — чем не устраивает полный и развернутый ответ в книге Мейерса?
Кстати, std::string в случае SSO (small string optimization), не относится к объектам с дешевым перемещением.khim
24.07.2019 15:05Кстати, std::string в случае SSO (small string optimization), не относится к объектам с дешевым перемещением.
Ээээ… А вы почему так решили???
Перемещение такой строки — это семь инструкций процессора вместо четырёх для «пустого буфера» того же размера.
И то — последние три инструкции нужны только из-за того, что компилятор «не видит», что полученная строка более не нужна. Если видит — то разницы в перемещении строки и буфера соотвесттвующего размера нет вообще.encyclopedist
24.07.2019 17:46Ээээ… А вы почему так решили???
В данном контексте цену перемещения нужно измерять не саму по себе, а по отношению к копированию. И для коротких строк перемещение оказывается таким же как и копия. А следовательно, метод "передача по значению + move" оказывается примерно вдвое дороже, чем "передача по константной ссылке + копия", например.
khim
24.07.2019 18:52В данном контексте цену перемещения нужно измерять не саму по себе, а по отношению к копированию.
Ok, принято, давайте сравним. Семь инструкций — перемещение, двести инструкций — копирование. Минипулистическая разница, афигеть! Даже код, который исполняется для коротких строк — это где-то 12-15 инструкций вместо 7 (смотря по какой «ставке» защитать «лишние» push'ы и pop'ы).
Кстати это всё для случаяstd::basic_string<int>
, если использовать простоstd::string
, то у вас там материализуется просто вызов конструктора — а это весьма дорого, на самом деле, дороже, чем перемещение строки, независимо от того, что там внутри самого этого конструктора происходит.
Да, в некоторых случаях копирования строк будет достаточно дешёвым (ради того SSO и существует, собственно). Но в общем случае… операция перемещения строки — это дёшево, копирования — «заметно дороже» (если строка всё-таки не «короткая», то её копирование — это весьма немалая работа… причём разница там не на проценты, а в разы). В типичной программе — разница обычно в 3-5 раз, насколько я помню, хотя могут быть отклонения в разные стороны в зависимости от того, что это за строки и что вы с ними делаете. Чтобы разница была менее, чем двукратной — нужно будет стандартную библиотеку использовать нестандартным образом, чтобы они копирование коротких строк не отдельной функцией оформляла, а инлайнила (вы часто это делаете?).
А следовательно, метод «передача по значению + move» оказывается примерно вдвое дороже, чем «передача по константной ссылке + копия», например.
«передача по значению + move» не будет делать никаких копий, если они не нужны. И это будет заметно дешевле, чем если вам таки копию нужно будет сделать из-за того, что вы выбрали стратегию «передача по константной ссылке + копия».
Вариант, когда можно получить выигрыш от использования стратегии «передача по константной ссылке + копия» — это случай, когда избежать копирования строк почти никогда не удаётся, процент «длинных» строк не мал, а черезвычайно мал и, в довершение всего, когда вы собрали стандартную библиотеку особым образом.
Так что перед тем, как пытаться получить выигрыш от использования стратегии «передача по константной ссылке + копия» неплохо было для начала понять: а что это вы такое со строками делаете, что они вдруг у вас так часто копируются? Вы точно проstd::move
не забываете?
Выигрыш, вами описанный, так-то, в принципе, возможен — но чтобы его получить на практике вам придётся весьма и весьма попотеть… потому я бы остался, скорее, с тем, что написано в обсуждаемой тут статье. Если нет веских причин делать по-другому.
P.S. И это мы ещё обсудили класс, который специально делали таким, чтобы копирование было как можно более быстрым в большинстве случаев. Почти все остальные контейнеры ничего подобного не имеют, там перемещение — дешевле, а копирование — дороже, чем для строк.
Antervis
24.07.2019 22:08+1А следовательно, метод «передача по значению + move» оказывается примерно вдвое дороже, чем «передача по константной ссылке + копия», например.
однако копирование не короткой строки на несколько порядков больше этих трех инструкций и с лихвой их перекроет даже если у вас длинных строк — одна из тысячи.khim
25.07.2019 08:34Тут идёт игра на том, что если вы не
std::move
акте строку, а позволяете ей скопироваться, то будет вызываться и конструктор копирования и конструктор перемещения. Но это только в том случае, если строки копируются часто, а перемещаются редко. Так как «свежевычесленные» строки, как правило, перемещаются, то на практике это обычно не так.
Antervis
вы даже не рассмотрели вариант с rvalue/lvalue перегрузками, который очевидно быстрее (ни в одном случае не получите лишнего копирования, но для rvalue перегрузки на один мув меньше передачи по значению).
Overlordff
Такие параметры конструктора приводят к комбинаторному взрыву перегрузок. Самый православный вариант — шаблонный конструктор с forward reference, там тоже нет лишнего мува, но и конструктор всего один.
khim
Не могли бы вы привести пример, когда это «очевидно быстрее» выразится в измеримых секундах при использовании современных компиляторов (clang 8+, gcc 9+, etc) — и после этого можно будет уже поговорить насколько это часто встречается в жизни.
Заранее спасибо.
twinklede
Пример чего? Того, что там будет копирование? Это очевидно.
То, что это выражается — так же очевидно. Но вы начните с измерений move — после этого уже можно будет поговорить о копировании.
Заранее спасибо.
khim
А давайте и попробуем. Заодно уж и на PF посмотрим. Вот такой вот PF:
Годится? А к нему — вот такой вот
std::move
: Пойдёт?А вот и результаты замеров:
Как видим
std::move
— ничего не стоит (как и ожидалось), а вот ваш любимый pf… да, таки весьма небесплатен.P.S. Только не нужно рассказывать сказок о том, что так никто писать не будет и вообще пример из пальца высосан. Ибо я вполне наблюдал подобные эффекты в коде «уверовавших в pf» — вполне себе в production, не в специальных тестах. Могу даже попробовать рассказть где. А вот эффектов, на которые жалуется Antervis… не наблюдал. Я не говорю, что их нет и никогда не бывает — я просто предлагаю обсуждать их на примерах похожих на реальные. Где вы хотя бы могли объяснить — где вы видели подобный код, как часто и почему. Вот только после этого — можно будет решить: насколько это практически полезный совет.
Antervis
тестировать мув против копирования на тривиальных типах? Это что-то новенькое.
как вам уже сказали — они очевидны. А если нет — внемлите. Напомню: бывают не только классы с дорогим копированием, но и с дорогим мувом.
khim
Разумеется. Любая экзотика, которую кто-либо когда-либо придумал может встретиться в реальном мире, если её специально не запретили.
Вопрос ведь не в этом. Вопрос, это… в цене вопроса. Насколько часто вы встречаете такие типы, насколько велика вероятность того, что для них вы не сделаете правильного конструктора и насколько разрушительным будут последствия для скорости работы программы и её размера.
Так вот, практически я ни разу не встречал класса, у которого был бы дорогой оператор перемещения, но при этом существовал бы оператор копирования. Вот ни разу. Да, наверное такие бывают, но… не встречал. Если вы с таким сталкиваетесь регулярно — то хоть расскажите откуда они берутся-то.
В то же время тривиальные типы (а «тривиальные типы», могут быть, в общем-то, и «замороченными»,
std::tuple<std::complex<float>, std::complex<float>>
, например) — встречаются куда чаще.На моей практике «инвертированное» правило из Мейрса (используйте
Foo
в качестве параметра конструктора в тех случаях, когда тип может копироваться иFoo&&
в тех случаях, когда это невозможно) работает куда лучше, чем «камлание» на pf.Antervis
бустовые small_vector, flat_set/flat_map являются типовыми примерами.
в вашем бенчмарке вы эксплуатировали особенности соглашения о вызовах x86. Тривиальные типы с несколькими полями могут не влазить в xmm и передаваться через стек. Тогда передача rvalue будет малость дешевле чем по значению.
В целом я согласен, что в подавляющем большинстве случаев передача по значению дает простейший код и сабоптимальное быстродействие. В статье, как я и отметил, меня смутило то, что автор даже не рассмотрел оптимальные варианты.
khim
move
дешёвый.Статья, если бы вы внимательно её прочитали — вообще не о тонкостях C++. Она о подходах C++ в сравнении с другими языками. Делать акцент на варианты, которые используются редко — в этом случае было бы странно.
encyclopedist
Любой класс, написанный до C++11, или где автор просто забыл написать конструктор перемещения. И таких классов в реальном коде полно.
khim
А почему, собственно, «таких классов в реальном коде полно»… в 2019м году? И почему вы их разводите?
Я ничего, в общем, не имею против классов, которые нельзя копировать вообще — это нормально. Их принимать через ссылку — вполне разумно (собственно а как их иначе принять?). Но вот класс, который можно копировать, но нельзя перемещать… вы гигиеной кода в приниципе никогда не занимаетесь? Или считаете что раз написанный код никогда не должен меняться?
Просто удивляет этот подход: мы, когда-то давным-давно, сделали плохой дизайн… потому что тогда, давным-давно, ничего лучшего не получалось… давайте теперь городить костыли до скончания веков… так что ли?
encyclopedist
А вам не приходило в голову, что люди иногда пользуются сторонними библиотеками?
Например, я на работе пользуюсь большим фреймворком, написанным начиная с ранних 90-х. Несколько лет назад авторы начали переделывать его на C++11, избавились от своего самописного аналога
auto_ptr
, от своего костыля заменяющего перемещение, и т.д., но работа не закончена.khim
В этом случае вам остаётся только посочувствовать… и, разумеется, вам придётся следовать гайдлайнам этой библиотеки. Если речь идёт о том, чтобы получить результат «здесь и сейчас».
Но вот использовать какой-нибудь
boost::container::small_vector
в случае, если у вас его ещё нет — я бы не стал. Выигрыш, скорее всего, себя не окупит.encyclopedist
Я посмотрел код, и в
boost::small_vector
вроде определены конструктор перемещения и перемещающий оператор присваивания. Так что не знаю, на что жаловался Anterviskhim
Они-то определены, но там оператор перемещения дорогой. Если он материализуется — получается куча манипуляций. Уж лучше бы его совсем не было.
encyclopedist
И да, откуда вы взяли вот это:
Я ничего такого не говорил, опять вы выдумываете.
Я говорил про классы без определенного конструктора перемещения, у которых перемещение автоматически превращается в копию.
khim
Вся идея получения объектов в конструкторе (вместо ссылок), которые вы потом
std::move
аете в нужное вам поле — заключается в том, чтобы избежать комбинаторного взрыва (который в ином случае неизбежен: даже если вы используете шаблоны и pf — компилятору всё равно придётся породить множество вариантов, даже если у вас в коде конструктор будет один).Для объектов, которые вы можете перемещать, но не копировать — этой проблемы не существует (строго говоря и для объектов, которые можно копировать, но не перемещать — тоже… но это такая экзотика, что я об этом даже не задумывался никогда), так зачем для них этот трюк использовать?
Замечание принимается. Я просто как-то позабыл о том, что в 2019м году такие классы могут использоваться где-либо, кроме «сурового Legacy». Но для них комбинаторного взрыва тоже нет, так что можно спокойно использовать ссылки, никакого PF, опять-таки, заводить не требуется.
Наверное можно попробовать использовать PF для того, чтобы разработать «универсальный путь к счастью»… но мне кажется, что проще начать, наконец, добавлять конструкторы перемещения в подобные классы — особенно если речь идёт о чём-то достаточно тяжёлом для того, чтобы копирование было неприемлемо по производительности.
encyclopedist
Опять вы отвечаете на что-то, чего я никогда не говорил. Я не призывал использовать PF, вы меня путаете с кем-то другим.
Я же только хотел указать, что есть случаи, когда "передача по значению + move" является плохим вариантом. И да, в таком случае, единственный разумный вариант — это передавать по константной ссылке.
Это встречается гораздо чаще, чем вы полагаете. В науке и промышленности — так повсеместно. Потому что сам код не является продуктом, и его качество не сильно кого-то беспокоит. Ну и работает — не трожь во всей красе.
khim
Вот это удивляет больше, чем что-либо другое…
Siemargl
не пойдет. оптимизатор выкидывает цикл
godbolt.org/z/0l0bdB
khim
Я правильно понимаю, что вы никогда-никогда ничего не пишите в C++ так, чтобы он было в двух файлах?
Ну если так — то это совсем другая история, к обычному программированию на C++ это всё имеет мало отношения.
Я не знаю ни одного реального проекта, полностью реализованного в одной единице трансляции.
Siemargl
так просто было удобнее сунуть в годболт.
да и любые тесты надо проверять в ассемблере — тестировать вывод константы неинтересно.
я сам так обманывался при тестировании производительности много раз — даже хуже, думая что обманул оптимизатор, ан нет… =)
если что, то от вашей программы осталось
khim
Ну если вы суёте в godbolt «не думая», то кто ж вам судья. Я там командные строки, где два файла
test1.cc
иtest2.cc
не зря приводил.Siemargl
Хорошо, посмотрел ассемблер через objdump.
Цикл есть, но разницы в коде нет…
gcc 6.3
khim
Не знаю уж что и с чем вы там сравнивали. Если версии с
std::move
и без — то разницы и не должно быть (хотя иногда она есть: вот, например… кашу маслом, может, и не испортишь, а C++-программу лишнимstd::move
— можно).Но главное отличие — вот оно. Ссылка может быть сколь угодно
perfect
… но ссылка — это ссылка. Это не передача объекта по значению. То есть избавиться от того, чтобы думать… мы — всё-таки не можем.А это лишает предложения «используйте
std::forward
и pf — и компилятор сам разберётся» главного преимущества: нет, если компилятор не видит всей программы — он не разберётся… а там где видит — так он разберётся и без pf… так почему мы должены на pf молиться, вдруг?