Судя по комментам habr.com/ru/post/460831/#comment_20416435 в соседнем посте и развернувшейся там дискуссии, на Хабре не помешает статья, как правильно передавать аргументы в конструктор или сеттер. На StackOverflow подобного материала полно, но тут что-то я не припомню.

Потому что пример в той статье полностью корректен, и автор статьи абсолютно прав. Вот этот пример:

// Хорошо.
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)


  1. Antervis
    22.07.2019 21:39

    вы даже не рассмотрели вариант с rvalue/lvalue перегрузками, который очевидно быстрее (ни в одном случае не получите лишнего копирования, но для rvalue перегрузки на один мув меньше передачи по значению).


    1. Overlordff
      22.07.2019 21:44

      Такие параметры конструктора приводят к комбинаторному взрыву перегрузок. Самый православный вариант — шаблонный конструктор с forward reference, там тоже нет лишнего мува, но и конструктор всего один.


    1. khim
      23.07.2019 05:00

      Не могли бы вы привести пример, когда это «очевидно быстрее» выразится в измеримых секундах при использовании современных компиляторов (clang 8+, gcc 9+, etc) — и после этого можно будет уже поговорить насколько это часто встречается в жизни.

      Заранее спасибо.


      1. twinklede
        23.07.2019 05:05

        Не могли бы вы привести пример

        Пример чего? Того, что там будет копирование? Это очевидно.

        выразится в измеримых секундах

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

        Заранее спасибо.


        1. khim
          23.07.2019 06:39

          Не могли бы вы привести пример
          Пример чего? Того, что там будет копирование? Это очевидно.
          Пока что очевидно только то, что реального кода у вас нет, есть только упоение собственной крутостью.

          Но вы начните с измерений move — после этого уже можно будет поговорить о копировании.
          А давайте и попробуем. Заодно уж и на PF посмотрим. Вот такой вот PF:
          #include <utility>
          
          template <typename T1, typename T2>
          long foo(T1&& x, T2&& y) {
            return std::forward<T1>(x) + std::forward<T2>(y);
          }
          
          template long foo<long, long>(long&&, long&&);
          
          long foo(long x, long y) {
            return x + y;
          }
          Годится? А к нему — вот такой вот std::move:
          #include <iostream>
          
          #ifdef WITH_PF
          template <typename T1, typename T2>
          long foo(T1&& x, T2&& y);
          #else
          long foo(long x, long y);
          #endif
          
          int main() {
            long x = 0;
            for (long i = 0; i < 1000000000; ++i) {
          #if defined(WITH_MOVE) || defined(WITH_PF)
              long y = i;
              x = foo(std::move(x), std::move(y));
          #else
              x = foo(x, i);
          #endif
            }
            std::cout << x << std::endl;
          }
          Пойдёт?

          А вот и результаты замеров:
          $ g++ -O3 test1.cc test2.cc -o without_move && time ./without_move
          499999999500000000
          
          real	0m5.979s
          user	0m5.971s
          sys	0m0.008s
          $ g++ -O3 test1.cc test2.cc -DWITH_MOVE -o with_move && time ./with_move
          499999999500000000
          
          real	0m5.975s
          user	0m5.973s
          sys	0m0.001s
          $ g++ -O3 test1.cc test2.cc -DWITH_PF -o with_pf && time ./with_pf
          499999999500000000
          
          real	0m7.374s
          user	0m7.361s
          sys	0m0.008s
          
          Как видим std::move — ничего не стоит (как и ожидалось), а вот ваш любимый pf… да, таки весьма небесплатен.

          P.S. Только не нужно рассказывать сказок о том, что так никто писать не будет и вообще пример из пальца высосан. Ибо я вполне наблюдал подобные эффекты в коде «уверовавших в pf» — вполне себе в production, не в специальных тестах. Могу даже попробовать рассказть где. А вот эффектов, на которые жалуется Antervis… не наблюдал. Я не говорю, что их нет и никогда не бывает — я просто предлагаю обсуждать их на примерах похожих на реальные. Где вы хотя бы могли объяснить — где вы видели подобный код, как часто и почему. Вот только после этого — можно будет решить: насколько это практически полезный совет.


          1. Antervis
            23.07.2019 07:39

            тестировать мув против копирования на тривиальных типах? Это что-то новенькое.

            А вот эффектов, на которые жалуется Antervis… не наблюдал

            как вам уже сказали — они очевидны. А если нет — внемлите. Напомню: бывают не только классы с дорогим копированием, но и с дорогим мувом.


            1. khim
              23.07.2019 09:10

              тестировать мув против копирования на тривиальных типах? Это что-то новенькое.
              Уверяю вас: «тривиальные» типы встречаются в программах на два, а то и три порядка чаще, чем те типы, для которых pf и связанные с ним сложности (комбинаторный взрыв и всю возня с инстанциированием нужных вам типов аргументов) имеет смысл.

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

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

              Так вот, практически я ни разу не встречал класса, у которого был бы дорогой оператор перемещения, но при этом существовал бы оператор копирования. Вот ни разу. Да, наверное такие бывают, но… не встречал. Если вы с таким сталкиваетесь регулярно — то хоть расскажите откуда они берутся-то.

              В то же время тривиальные типы (а «тривиальные типы», могут быть, в общем-то, и «замороченными», std::tuple<std::complex<float>, std::complex<float>>, например) — встречаются куда чаще.

              На моей практике «инвертированное» правило из Мейрса (используйте Foo в качестве параметра конструктора в тех случаях, когда тип может копироваться и Foo&& в тех случаях, когда это невозможно) работает куда лучше, чем «камлание» на pf.


              1. Antervis
                23.07.2019 09:38

                Вот ни разу. Да, наверное такие бывают, но… не встречал

                бустовые small_vector, flat_set/flat_map являются типовыми примерами.

                В то же время тривиальные типы (а «тривиальные типы», могут быть, в общем-то, и «замороченными», std::tuple&ltstd::complex&ltfloat&gt, std::complex&ltfloat&gt&gt, например) — встречаются куда чаще.

                в вашем бенчмарке вы эксплуатировали особенности соглашения о вызовах x86. Тривиальные типы с несколькими полями могут не влазить в xmm и передаваться через стек. Тогда передача rvalue будет малость дешевле чем по значению.

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


                1. khim
                  23.07.2019 11:51

                  бустовые small_vector, flat_set/flat_map являются типовыми примерами.
                  И, думаю, по этой самой причине у нас они и не используются. flat_map/flat_set у нас свои — у них move дешёвый.

                  В статье, как я и отметил, меня смутило то, что автор даже не рассмотрел оптимальные варианты.
                  Статья, если бы вы внимательно её прочитали — вообще не о тонкостях C++. Она о подходах C++ в сравнении с другими языками. Делать акцент на варианты, которые используются редко — в этом случае было бы странно.


              1. encyclopedist
                23.07.2019 11:03

                Так вот, практически я ни разу не встречал класса, у которого был бы дорогой оператор перемещения, но при этом существовал бы оператор копирования. Вот ни разу. Да, наверное такие бывают, но… не встречал. Если вы с таким сталкиваетесь регулярно — то хоть расскажите откуда они берутся-то.

                Любой класс, написанный до C++11, или где автор просто забыл написать конструктор перемещения. И таких классов в реальном коде полно.


                1. khim
                  23.07.2019 12:13

                  А почему, собственно, «таких классов в реальном коде полно»… в 2019м году? И почему вы их разводите?

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

                  Просто удивляет этот подход: мы, когда-то давным-давно, сделали плохой дизайн… потому что тогда, давным-давно, ничего лучшего не получалось… давайте теперь городить костыли до скончания веков… так что ли?


                  1. encyclopedist
                    23.07.2019 12:19

                    А вам не приходило в голову, что люди иногда пользуются сторонними библиотеками?


                    Например, я на работе пользуюсь большим фреймворком, написанным начиная с ранних 90-х. Несколько лет назад авторы начали переделывать его на C++11, избавились от своего самописного аналога auto_ptr, от своего костыля заменяющего перемещение, и т.д., но работа не закончена.


                    1. khim
                      23.07.2019 12:53

                      А вам не приходило в голову, что люди иногда пользуются сторонними библиотеками?
                      Приходило. Но мне казалось, что сторонние библиотеки используются тогда, когда они облегчают написание кода, а не усложняют его.

                      Например, я на работе пользуюсь большим фреймворком, написанным начиная с ранних 90-х.
                      В этом случае вам остаётся только посочувствовать… и, разумеется, вам придётся следовать гайдлайнам этой библиотеки. Если речь идёт о том, чтобы получить результат «здесь и сейчас».

                      Но вот использовать какой-нибудь boost::container::small_vector в случае, если у вас его ещё нет — я бы не стал. Выигрыш, скорее всего, себя не окупит.


                      1. encyclopedist
                        23.07.2019 13:20

                        Но вот использовать какой-нибудь boost::container::small_vector в случае, если у вас его ещё нет — я бы не стал. Выигрыш, скорее всего, себя не окупит.

                        Я посмотрел код, и в boost::small_vector вроде определены конструктор перемещения и перемещающий оператор присваивания. Так что не знаю, на что жаловался Antervis


                        1. khim
                          23.07.2019 17:37
                          +1

                          Они-то определены, но там оператор перемещения дорогой. Если он материализуется — получается куча манипуляций. Уж лучше бы его совсем не было.


                  1. encyclopedist
                    23.07.2019 12:21

                    И да, откуда вы взяли вот это:


                    Но вот класс, который можно копировать, но нельзя перемещать…

                    Я ничего такого не говорил, опять вы выдумываете.


                    Я говорил про классы без определенного конструктора перемещения, у которых перемещение автоматически превращается в копию.


                    1. khim
                      23.07.2019 13:04

                      Но вот класс, который можно копировать, но нельзя перемещать…
                      Я ничего такого не говорил, опять вы выдумываете.
                      Об этом говорили не вы, но Скотт Мейерс.

                      Вся идея получения объектов в конструкторе (вместо ссылок), которые вы потом std::moveаете в нужное вам поле — заключается в том, чтобы избежать комбинаторного взрыва (который в ином случае неизбежен: даже если вы используете шаблоны и pf — компилятору всё равно придётся породить множество вариантов, даже если у вас в коде конструктор будет один).

                      Для объектов, которые вы можете перемещать, но не копировать — этой проблемы не существует (строго говоря и для объектов, которые можно копировать, но не перемещать — тоже… но это такая экзотика, что я об этом даже не задумывался никогда), так зачем для них этот трюк использовать?

                      Я говорил про классы без определенного конструктора перемещения, у которых перемещение автоматически превращается в копию.
                      Замечание принимается. Я просто как-то позабыл о том, что в 2019м году такие классы могут использоваться где-либо, кроме «сурового Legacy». Но для них комбинаторного взрыва тоже нет, так что можно спокойно использовать ссылки, никакого PF, опять-таки, заводить не требуется.

                      Наверное можно попробовать использовать PF для того, чтобы разработать «универсальный путь к счастью»… но мне кажется, что проще начать, наконец, добавлять конструкторы перемещения в подобные классы — особенно если речь идёт о чём-то достаточно тяжёлом для того, чтобы копирование было неприемлемо по производительности.


                      1. encyclopedist
                        23.07.2019 13:17

                        Опять вы отвечаете на что-то, чего я никогда не говорил. Я не призывал использовать PF, вы меня путаете с кем-то другим.


                        Я же только хотел указать, что есть случаи, когда "передача по значению + move" является плохим вариантом. И да, в таком случае, единственный разумный вариант — это передавать по константной ссылке.


                        Я просто как-то позабыл о том, что в 2019м году такие классы могут использоваться где-либо, кроме «сурового Legacy».

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


                        1. khim
                          23.07.2019 17:43

                          Потому что сам код не является продуктом, и его качество не сильно кого-то беспокоит.
                          Зачем он тогда создаётся на языке, единственным достоинством которого является строгое следование принципу «не плати за то, что не используешь» (что, собственно, даёт возможность сделать качественный код — и, в общем, ничего более)?

                          Вот это удивляет больше, чем что-либо другое…


          1. Siemargl
            24.07.2019 13:21

            не пойдет. оптимизатор выкидывает цикл
            godbolt.org/z/0l0bdB


            1. khim
              24.07.2019 14:35

              Я правильно понимаю, что вы никогда-никогда ничего не пишите в C++ так, чтобы он было в двух файлах?

              Ну если так — то это совсем другая история, к обычному программированию на C++ это всё имеет мало отношения.

              Я не знаю ни одного реального проекта, полностью реализованного в одной единице трансляции.


              1. Siemargl
                24.07.2019 14:41

                так просто было удобнее сунуть в годболт.

                да и любые тесты надо проверять в ассемблере — тестировать вывод константы неинтересно.
                я сам так обманывался при тестировании производительности много раз — даже хуже, думая что обманул оптимизатор, ан нет… =)

                если что, то от вашей программы осталось

                cout << 499999999500000000; 


                1. khim
                  24.07.2019 14:55

                  Ну если вы суёте в godbolt «не думая», то кто ж вам судья. Я там командные строки, где два файла test1.cc и test2.cc не зря приводил.


                  1. Siemargl
                    24.07.2019 15:17

                    Хорошо, посмотрел ассемблер через objdump.
                    Цикл есть, но разницы в коде нет…
                    gcc 6.3


                    1. khim
                      24.07.2019 16:23

                      Не знаю уж что и с чем вы там сравнивали. Если версии с std::move и без — то разницы и не должно быть (хотя иногда она есть: вот, например… кашу маслом, может, и не испортишь, а C++-программу лишним std::move — можно).

                      Но главное отличие — вот оно. Ссылка может быть сколь угодно perfect… но ссылка — это ссылка. Это не передача объекта по значению. То есть избавиться от того, чтобы думать… мы — всё-таки не можем.

                      А это лишает предложения «используйте std::forward и pf — и компилятор сам разберётся» главного преимущества: нет, если компилятор не видит всей программы — он не разберётся… а там где видит — так он разберётся и без pf… так почему мы должены на pf молиться, вдруг?


  1. ncr
    22.07.2019 21:50
    -1

    Такой код позволяет покрыть все возможные варианты использования класса
    Правда?


    1. myxo
      22.07.2019 23:36
      +1

      Ну так все правильно.
      string_view — это специальный класс, который не владеет данными. Если вы хотите сделать копию данных и передать её в класс, это нужно делать явно на стороне вызывающего.


      1. ncr
        23.07.2019 00:08

        вы хотите сделать копию данных и передать её в класс, это нужно делать явно на стороне вызывающего

        Я? Нет, лично я не хочу делать копию.
        Если class person хочет у себя внутри иметь копию строк — это его право и, в общем случае, детали реализации.
        Сваливая бремя конструирования этих строк на клиента, вы потенциально загрязняете ему код:
        std::string_view First = ..., Last = ...;
        person Person{std::string{First}, std::string{Last}};

        — Красота же, да?

        Принимайте string_view, он неявно и практически бесплатно конструируется из всего, и копируйте его потом себе как хотите (если надо).
        Единственный «недостаток» — да, нельзя переместить rvalue-строку внутрь объекта (и этим сэкономить аж одно копирование). Если у вас есть сомнения в эффективности — сначала профилируйте, и только если вы действительно можете что-то выиграть на этих временных объектах — оптимизируйте.


        1. 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, а какие-до данные произвольной длины. Или же объект произвольной структуры (общий случай).


        1. twinklede
          23.07.2019 03:39

          Единственный «недостаток» — да

          sv — не си-строка, но прозрачно создаётся из си-строки со всеми вытекающими.

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

          Никакой профайлер ничего не покажет. Подобные советы говорят лишь о крайне странных представлениях о профайлере и отсутствии опыта работы с ним(кроме как в примитивных случаях).

          Хорошо. Мы имеет оверхед на передаче. У нас тысячи функций, передаются десятки, сотни разных типов объектов. Всё это будет размазано и десяткам/сотням разных функций. Как мы вообще поймём, что проблема есть и как мы её локализуем? Да никак.

          Даже если мы увидим много memcpy/malloc, то это не всегда нам позволит найти и локализовать причину. Ну узнаем мы, что из вызывают эти десятки/сотни функции(вместе с её тысячами других). Какие из этих конструкторов лишние?

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


          1. khim
            23.07.2019 05:16

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

            Я с этим феноменом сталкивался много раз: люди считают, что если они могут ускорить код раза в 3-4 с помощью профайлера — то это указывает на то, что они всё сделали «правильно». Тот факт, что можно сделать «неправильно», так что профайлер применить куда-либо будет очень сложно, но всё будет работать ещё раз так в 5-6 быстрее… им обычно в голову не приходит.

            Всё это довольно-таки грустно и напоминает попытку поставить на автомобиль Формулы-1 автоматическую коробку передач и шипованную резину.

            Если вас устраивает вариант, когда код работает в 3-5 раз медленнее, чем он мог бы работать… и вы готовы с подобным замедлением мириться ради «красоты кода»… то зачем вы вообще связались с C++? C# или Java дадут вам желаемое с куда меньшими затратами!


            1. 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». ©
              Замеряйте.


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


              1. Antervis
                24.07.2019 21:51

                «Premature optimization is the root of all evil». ©

                Допустим, я знаю, что std::move «вот здесь» точно на любом нынешнем компиляторе сэкономит мне аллокацию/деаллокацию/копирование, и никогда не сделает хуже, почему бы мне этот мув не написать? Зачем мне доводить код до того состояния, когда его надо оптимизировать через профилировщик, указывающий на 40% нагрузку от memcpy (true story)? Тем более что многие такие оптимизациями даются «невероятным трудом» из категории «дописать unordered_». Я бы даже не назвал это преждевременной оптимизацией, скорее, предотвращением деградации.


          1. DustCn
            23.07.2019 16:01

            Подход профилируй-оптимизируй он правильнее чем вы начнете оптимизировать основываясь только на своих каких то предположениях. Не все профилировщики работают на уровне функций. Тот же Втюн вполне может спуститься на уровень базовых блоков (basic block) и показать потерю времени в куске, связанном с конструктором.

            >>Даже если мы увидим много memcpy/malloc
            Уже будет хорошим поинтом чтобы насторожиться. Дальше в том же профилировщике открывается статистика по call site.


            1. khim
              23.07.2019 18:17

              Тот же Втюн вполне может спуститься на уровень базовых блоков (basic block) и показать потерю времени в куске, связанном с конструктором.
              Однако ни один VTune не может рассказать вам как избежать кеш-промахов… а это — самое важное, что есть в оптимизации вообще. Один промах с необходимостью похода в память — эквивалентен сотням операций и даже L2/L3 даёт не такой большой выигрыш, чтобы этим можно было пренебречь.

              L1 же имеет размер, во-первых крошечный, а во-вторых — фактически не меняющийся со временем: Zen2 имеет кеш в 32Kb, то есть примерно столько же, сколько было в кеше PowerPC 601 четверть века назад!

              Уже будет хорошим поинтом чтобы насторожиться. Дальше в том же профилировщике открывается статистика по call site.
              Ну обнаружили мы, что это произошло из-за очень SOLIDной архитектуры, которая «размазала» десяток счётчиков (которые нам реально нужны для реализации алгоритма) по множеству разнообразных, хитро связанных между собой структур данных, коих у нас насчитывается порядка полутысячи (реальный пример из реальной программы). Ваши дальнейшие действия?


              1. DustCn
                23.07.2019 21:28

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

                >«размазала» десяток счётчиков (которые нам реально нужны для реализации алгоритма) по множеству разнообразных, хитро связанных между собой структур данных, коих у нас насчитывается порядка полутысячи…

                Структуры не исполняются. Оптимизировать надо в первую очередь бутылочные горлышки, а не лазить по зиллиону сеттеров-геттеров.
                Если у вас в приложении конструктор занимает 80% времени значит вы изначально что-то не то делаете.


                1. khim
                  24.07.2019 11:29

                  Структуры не исполняются.
                  Исполняется код, «ползающий» по этим структурам.

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

                  Оптимизировать надо в первую очередь бутылочные горлышки, а не лазить по зиллиону сеттеров-геттеров.
                  Это если вам нужна занятость и вы хотите в течение 10 лет каждый месяц отчитываться об ускорении на 5-10-15% (вначале больле, потом меньше).

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

                  Если у вас в приложении конструктор занимает 80% времени значит вы изначально что-то не то делаете.
                  Совершенно не обязательно. Если у вас 80% времени уходит на конструктор вспомогательного объекта, который вам, в сущности, не нужен — то это одно. А если это — часть «внутреннего цикла», делающего основную работу — то совсем другое.

                  Гляньте как-нибудь на профайл «вылизанных до упора» энкодеров видео. 80% «на конструктор» вы там, конечно, не увидите — но там будут весьма и весьма «горячие»' участки. Где будет как раз, суммарно, 80% времени проходить.

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


                  1. DustCn
                    24.07.2019 13:12

                    Это если вам нужна занятость и вы хотите в течение 10 лет каждый месяц отчитываться об ускорении на 5-10-15% (вначале больле, потом меньше).

                    Ну лазьте по ним и отчитывайтесь, я не понимаю вы спорите ради спора?

                    Если же вы хотите сделать быстрый код — то заниматься нужно совсем другим.

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

                    Совершенно не обязательно. Если у вас 80% времени уходит на конструктор вспомогательного объекта, который вам, в сущности, не нужен — то это одно. А если это — часть «внутреннего цикла», делающего основную работу — то совсем другое.

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

                    Гляньте как-нибудь на профайл «вылизанных до упора» энкодеров видео. 80% «на конструктор» вы там, конечно, не увидите — но там будут весьма и весьма «горячие»' участки. Где будет как раз, суммарно, 80% времени проходить.
                    Глядел лет 15 назад, потом ушел в HPC. К чему это замечание — мне не очень понятно. Все оптимизаторы это 80% и ковыряют, забив на остальные 20.

                    А «ровный» спектр, где все «горячие участки» потушены — это как раз результат многолетнего бездумного применения VTune.

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


  1. encyclopedist
    23.07.2019 00:14

    Я попытался сделать сводную таблицу эффективности разных способов передачи


    image


  1. 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 на базе неё. Всё остальное — не является полноценным и эффективным. Даже в таком самом лучшем для «по значению» кейсе.


    1. DmitryKoterov Автор
      23.07.2019 03:10
      +1

      Напишите статью-опровержение. Только сначала перечитайте все примеры в этой, пожалуйста, я там немного добавил про std::forward в том числе.


      1. twinklede
        23.07.2019 03:59
        -2

        Напишите статью-опровержение.

        Мне лень.

        Только сначала перечитайте все примеры в этой, пожалуйста, я там немного добавил про std::forward в том числе.

        Я прочитал и именно про forward я и говорил. Я нигде не отрицал, что вы показывали pf. Я говорил о том, что ваши выводы/сравнения pf и «по значению» неправильные. Так же, я объяснял почему.

        Так же, я рассказал и о том, откуда взялся этот срыв покровов с «по-значению» с которым носятся неофиты. Они продолжают повторять одно и тоже уже сколько лет. Но проблема в том, что те кто изначально об этом рассказывал — объясняли всё. Но люди выдирают оттуда какие-то куски, ничего не понимания и неся эти откровения в массы.

        «по-значению» + move достаточно хайповая вещь, которая никогда не претендовала на замену pf. Так же, было чётко сказано о её применении.

        как правильно передавать аргументы в конструктор или сеттер.

        Про это я так же говорил. const & имеет только одну проблему — нельзя мувать. Мы можем передать временный объект, но не можем его замувать(т.к. мы не знаем — какой там объект).

        Именно поэтому «по-значению» имеет смысла только тогда, когда мы заходим мувать. Т.е. когда мы ходим скопировать/замувать аргумент в поле. Но, хоть и пример именно такой(а он чисто случайно такой), то нигде и никак на это явно не указывается.

        Так же, я не стал говорить о том, что существуют концепты. Есть/будет auto && | String && и pf уже не так сложно писать.


        1. 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 — всё ещё не так эффективен, как «сырой» указатель…


          1. DmitryKoterov Автор
            23.07.2019 11:17

            Так это… надо ж смотреть не только как функция в вакууме выглядит, а еще и как ее вызов заинлайнится. И на -O3.


            В случае заинлайнивания тоже unique_ptr будет выглядеть отлично от обычного указателя? Или даже если не заинлайнится, то не сгенерирует ли компилятор две версии пролога, один когда надо занулять оригинал и один когда не надо?


            Естественно что в вакууме ему надо занулить старый unique_ptr, о том и ассемблер.


            1. khim
              23.07.2019 12:18

              В случае заинлайнивания тоже unique_ptr будет выглядеть отлично от обычного указателя?
              В случае заинлайнивания — всё будет в порядке. Но тут вроде как неглупые люди «топят» за то, как раз, чтобы в интерфейсе использовать тоже std::unique_ptr.

              Или даже если не заинлайнится, то не сгенерирует ли компилятор две версии пролога, один когда надо занулять оригинал и один когда не надо?
              Нет, не сгенерирует. Вопрос не в необходимости «занулять оригинал», а в ABI. «Мелкие» структуры все современные компиляторы передают на регистрах… за исключением случая, когда оная структура содержит нетривиальный деструтктор… как несложно догадаться — «умный» указатель без нетривиального деструктора окажется, как бы помягче, не слишком «умным».

              Естественно что в вакууме ему надо занулить старый unique_ptr, о том и ассемблер.
              К сожалению ему не «в вакууме» нужно это делать. А в реальном коде с реальным ABI. А в вакууме, как раз, он может делать что угодно.


          1. encyclopedist
            23.07.2019 11:19
            +1

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

            Никто там такого не советует. Вы намеренно исказили совет и теперь его опровергаете.


            Вообще, khim, вас как будто подменили. Когда-то вы писали на забре разумные вещи, а последнее время часто несете какую-то чепуху.


            1. 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++ стандарта не существует.


  1. withkittens
    23.07.2019 04:02
    +1

    Вот здесь дядька разобрал большое количество способов передачи параметров.
    Ошибки и эффективность в malloc'ах.
    www.youtube.com/watch?v=PNRju6_yn3o
    Сам я в С++ не бум-бум, но посмотреть было интересно.


    1. twinklede
      23.07.2019 04:50

      Ошибки и эффективность в malloc'ах.

      Состоятельность подобного сравнения сомнительна, а вернее отсутствует. Существует не только malloc, а ещё и копирование. К тому же, на тех строках которые он показывал — аллокация и так не будет — будет оверхед только на копировании.

      Как я уже писал — существует множество объект тяжелее условно-бесплатных строк. Хотя даже они не так что-бы и совсем бесплатны.

      www.youtube.com/watch?v=PNRju6_yn3o

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

      В конечном итоге этот трюк сливает pf, но автор нашел вывод — это похаять pf за шаблоны. Но это не проблема pf. Это общая проблема шаблонов которые нельзя ограничить и приходится обкладываться sfinae. Но её уже решили концептами.

      К тому же, это решение не универсально. Оно работает только если нам нужно копировать объекты «как есть» вовнутрь объекта. Такое далеко не всегда нужно, а вернее чаще ненужно. А когда нужны все эти кейсы тривиальны и проще просто открыть все поля и инициализировать объекты через список инициализации. Это будет и проще и нагляднее и быстрее: cust{.first = «Niko»}


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


    1. myxo
      23.07.2019 11:06
      +2

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


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


    1. DmitryKoterov Автор
      23.07.2019 11:21

      Это “for in parameters”, т.е. где семантика владения не завязана. А в статье речь про конструкторы и сеттеры. Это более частный случай.


      1. khim
        23.07.2019 12:47

        Тем не менее формально это противоречит ES.56, который был процитирован выше.

        Но тут важно помнить, что «Core Guidelines» написаны с точки зрения кода, который затачивается под гипотетический «идеальный» компилятор C++99 (но который, по какой-то причине, вы всё-таки вынуждены писать и запускать с сегодняшними, далеко не идеальными, компиляторами).

        Если вас интересует вариант, который можно использовать «здесь и сейчас» — то далеко не все Core Guidelines оказываются «одинаково полезными».


  1. darkAlert
    23.07.2019 13:13

    Спасибо за статью! Я пользуюсь std::move, но как то всё руки не доходили использовать это для аргументов. Были мысли, что компиляторы умнее меня и можно не заморачиваться.


  1. klirichek
    24.07.2019 10:23

    По поводу "а зачем?" с шаблонным вариантом — смысл таки есть. Конечная цель — инициализировать член данных (а не создавать/копировать/перемещать временные объекты такого же типа; это уже детали реализации). Если класс данных умеет создаваться не только из подобных, а из неких совершенно других параметров, то шаблонный вариант просто пробросит их прямо к конечному конструктору члена, минуя вообще любое перемещение/копирование.


    1. khim
      24.07.2019 11:57

      Зато шаблонный вариант напорождает кучу кода во всех единицах трансляции.

      В зависимости от того, к чему вы стремитесь — это может быть и хорошо и плохо.

      Но в случае, если перемещение дешёвое, а копирование более дорогое (а это, всё-таки — типичная ситуация), лучше иметь один конструктор.


      1. 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, покуда всё равно конструируется и перемещается временный объект.


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

        Не видим вообще упоминаний; что, в общем-то, очевидно.
        В случае вызова с оптимизацией О2 g++ -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). Что тоже достаточно очевидно.


        1. khim
          24.07.2019 14:43

          Всё это хорошо выглядит на игрушечных примерах. А кончается тем, что наша внутрифирменная обёртка над Git (cloud-based, всё очень хайпово, да) занимает 500 мегабайт.

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

          Как я уже сказал: иногда — в этом есть смысл. Но нужно чётко отдавать себе отчёт в том, что, где и для чего вы желаете.


  1. Siemargl
    24.07.2019 13:29
    +1

    Как глупо породивший этот весь флейм в упомянутом топике таки спрошу — чем не устраивает полный и развернутый ответ в книге Мейерса?

    Кстати, std::string в случае SSO (small string optimization), не относится к объектам с дешевым перемещением.


    1. khim
      24.07.2019 15:05

      Кстати, std::string в случае SSO (small string optimization), не относится к объектам с дешевым перемещением.
      Ээээ… А вы почему так решили???

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

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


      1. encyclopedist
        24.07.2019 17:46

        Ээээ… А вы почему так решили???

        В данном контексте цену перемещения нужно измерять не саму по себе, а по отношению к копированию. И для коротких строк перемещение оказывается таким же как и копия. А следовательно, метод "передача по значению + move" оказывается примерно вдвое дороже, чем "передача по константной ссылке + копия", например.


        1. 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. И это мы ещё обсудили класс, который специально делали таким, чтобы копирование было как можно более быстрым в большинстве случаев. Почти все остальные контейнеры ничего подобного не имеют, там перемещение — дешевле, а копирование — дороже, чем для строк.


        1. Antervis
          24.07.2019 22:08
          +1

          А следовательно, метод «передача по значению + move» оказывается примерно вдвое дороже, чем «передача по константной ссылке + копия», например.

          однако копирование не короткой строки на несколько порядков больше этих трех инструкций и с лихвой их перекроет даже если у вас длинных строк — одна из тысячи.


          1. khim
            25.07.2019 08:34

            Тут идёт игра на том, что если вы не std::moveакте строку, а позволяете ей скопироваться, то будет вызываться и конструктор копирования и конструктор перемещения. Но это только в том случае, если строки копируются часто, а перемещаются редко. Так как «свежевычесленные» строки, как правило, перемещаются, то на практике это обычно не так.