Передача по значению и оптимизация исключения копирования

Я наткнулся на статью http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

Совет автора:

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

Однако я не совсем понимаю, какие преимущества получаются в двух примерах, представленных в статье:

//Don't
    T& T::operator=(T const& x) // x is a reference to the source
    { 
        T tmp(x);          // copy construction of tmp does the hard work
        swap(*this, tmp);  // trade our resources for tmp's
        return *this;      // our (old) resources get destroyed with tmp 
    }

vs

// DO
    T& operator=(T x)    // x is a copy of the source; hard work already done
    {
        swap(*this, x);  // trade our resources for x's
        return *this;    // our (old) resources get destroyed with x
    }

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


person newprint    schedule 16.09.2013    source источник
comment
Если источник является временным: obj = T(); или obj = foo();, где foo() возвращает T.   -  person juanchopanza    schedule 16.09.2013
comment
На самом деле это не очень хороший совет. Он раскрывает то, что должно быть деталью реализации (независимо от того, копируете ли вы аргумент или нет) в интерфейсе, что является чрезвычайно плохой разработкой программного обеспечения. Могут быть случаи, когда профилировщик говорит, что вы должны, но в остальном вы придерживаетесь правил кодирования. (Кажется, вездесущая рекомендация — передавать типы классов по ссылке, а все остальное — по значению, хотя это тоже можно рассматривать как преждевременную оптимизацию.)   -  person James Kanze    schedule 16.09.2013
comment
@jameskanze, эта функция копирует состояние аргумента, является разумной функцией интерфейса. Среди прочего, он влияет на стоимость, он сообщает вам, какие функции типа аргументов должны быть реализованы, и даже информирует пользователя о функциональности. Часть гениальности C++11 заключалась в том, что копирование и перемещение являются важными операциями с данными, а их раскрытие важно.   -  person Yakk - Adam Nevraumont    schedule 16.09.2013


Ответы (1)


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

extern T f();
...
T value;
value = f();

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

person Dietmar Kühl    schedule 16.09.2013
comment
Существует также разница в отношении исключений. В случае T const& конструктор копирования может генерировать исключение внутри вызываемой функции, но в случае T исключение будет генерироваться в контексте вызывающей программы (это означает, что оператор присваивания может быть помечен как noexcept, даже если конструктор копирования может выдать исключение). - person Simple; 16.09.2013
comment
@Simple: На самом деле это интересный момент! Хотя, в конце концов, это не имеет большого значения: все выражение все равно может бросить. Это может упростить работу с шаблонами, которым в результате не нужно возиться с условным noexcept. - person Dietmar Kühl; 16.09.2013
comment
Копию во втором случае также можно заменить ходом, который является районным от элизиона. - person Yakk - Adam Nevraumont; 16.09.2013
comment
@DietmarKühl Спасибо за ваш ответ, к сожалению, ваш ответ не имеет для меня никакого смысла. Итак, позвольте мне перефразировать ваш ответ и попытаться понять, имеет ли он смысл (для вас и других читателей). Вот что я получил на данный момент: у нас есть два operator(s)= first, один из которых является обычным оператором присваивания класса T::operator=(...), а второй - обычным (функцией) operator=(...). Следуя вашему коду, есть две возможности для окончательного назначения. - person newprint; 17.09.2013
comment
@DietmarKühl Первая возможность, value::operator=(f()), где временный аргумент rvalue привязывается к ссылочному параметру const lvalue (вы можете это сделать, поскольку это const), и поскольку параметр x является lvalue внутри (* как это обсуждалось в статье), он будет скопировано в tmp несмотря ни на что, т.е. мы не можем исключить копию. В перспективе: {temp_anon} создано - одна копия, tmp - вторая копия -- Занята память на 2 объекта + 2 вызова ctor + 2 вызова dtor - person newprint; 17.09.2013
comment
@DietmarKühl Вторая возможность, operator=(T x) называется. то есть operator=(f()); В этом случае x уже создан за нас компилятором, и нам не нужно его копировать, как в предыдущем. В перспективе: {temp_anon} создано - одна копия -- Память для одного объекта занята + 1 вызов ctor + 1 вызов dtor. Я не ошибаюсь ? - person newprint; 17.09.2013
comment
@newprint: Да, дело в том, что копии могут быть полностью исключены при передаче по значению, в то время как при передаче по ссылке требуется как минимум дополнительная копия. Как было указано в комментариях, есть два дополнительных преимущества: даже если копию нельзя исключить, параметр значения можно переместить, и любое исключение будет вызвано еще до вызова оператора присваивания. - person Dietmar Kühl; 17.09.2013
comment
Если аргумент возвращается, использование аргумента по значению отключает NRVO внутри этой функции, похоже. Поэтому, если он поощряет оптимизацию для передачи, будьте осторожны, что вы на самом деле делаете с этой копией. - person JDługosz; 19.11.2015