Pull to refresh

Comments 20

Чисто профессиональное замечание, перевод:
вы написали для класса конструктор копирования, который может делать все, и ожидаете
не равнозначен оригиналу:
you wrote a copy constructor for your class that could do anything
и более, того из-за этого теряется смысл всей статьи. Суть в некорректном переводе anything.
В англоязычном оригинале подразумевалось, что в конструкторе копирования можно делать всё что захочется, и что программист ожидает это поведение, то есть, что конструктор копирования будет вызван и какая-то часть кода, которая присутствует только там, будет вызвана соответственно, но из-за того, что компилятор удалил копию, это не произойдёт и часть кода не только не будет вызвана, а будет удалена.
В оригинале это как раз указано:
If compilers were to unpredictably remove copies, and thus remove pairs of copy/move constructor & destructor calls, they might break your code.
а вот перевод гораздо менее точный
Если компиляторы будут непредсказуемым образом удалять копии, тем самым также удаляя пары конструкторов & деструкторов копирования/перемещения, то это может привести к нарушению кода.
хотя формально он правильный, только более обтекаемый, обобщённый, что ли. Но именно это и ломает весь смысл фразы и статьи в итоге.

И какие практические правила для эффективной передачи объектов?


  • Возврат "тяжелого" объекта из функции.
    • Например, я хочу вернуть вектор, строку итп. Как лучше всего поступать? Насколько мне известно, основная рекомендация: возвращать по значению и положиться на RVO/NVRO. Может ли тут быть ситуация, когда это не сработает? Как можно подсказать компилятору?
  • Перемещение (передача владения) в функцию
    • Привести к rvalue с помощью std::move?

А еще где-то неподалеку perfect forwarding...

В "return expr;" expr всегда является rvalue, то есть будет вызван конструктор перемещения, если он есть. Поэтому вектор можно спокойно возвращать по значению (без std::move)

Более того, если возвращать return std::move(expr); то rvo не будет, а будет конструктор перемещения всегда

Для собственного развития: а есть практические примеры в которых имеет смысл так делать?

Честно говоря не знаю. Может если компилятор не поддерживает rvo

так имеет смысл делать, если хочется переместить member структуры или класса

struct A { std::string s; };

std::string foo()
{
A a;
//...
return std::move(a).s; //если просто сделать a.s, то будет копирование
}
Уточню, что «всегда» относилось к контексту темы NRVO. В общем случае это не так, конечно.
Если не хочется задумываться о нюансах и потенциальных проблемах, можно возвращать через аргумент функции по ссылке/указателю, а не через return.
Но с учётом того, что возвращаемый объект модифицируется в процессе работы фукнции и, если функция раньше времени упадёт, то может остаться неоделанный объект, если исключение неправильн обработано будет.
Насколько мне известно, основная рекомендация: возвращать по значению и положиться на RVO/NVRO. Может ли тут быть ситуация, когда это не сработает?
Скажем, в таком случае RVO сработает:
std::string foo = ...;
return foo;
а в таком нет:
std::string foo = ...;
std::string bar = ...;
return cond ? foo : bar;
поэтому во втором случае лучше добавлять std::move. Подробнее можно почитать у Майерса, он хорошо эти случаи разбирает.

А зачем вам ассемблерный вывод, заставили бы copy ctor и move ctor писать что-нибудь в cout, и всё, не надо дизассемблировать.


Upd: формально, не нашёл вашей ситуации в стандарте. Так что я так понимаю стандарт здесь не даёт никаких гарантий по copy elision.

какая-то не подробная статья. Рассмотрен лишь один пример, да и тот мягко говоря тривиален
И, конечно же, вы ожидаете, что, согласно правилам С++, этот конструктор будет вызван всякий раз когда объект вашего класса копируется. Но если компиляторы будут непредсказуемо удалять копирование, тем самым удаляя пары вызовов копирующего/перемещающего конструктора и деструктора они могут разрушить всю логику вашего кода.
RVO была допустима еще до с++11, и в каждом следующем стандарте расширяется набор кейсов, в которых компилятор вправе удалить лишние копирования/мувы. Так что если ваш код полагается на сайд эффекты копирований/мувов, то вы сам себе злобный буратино.
Автор оригинальной статьи забыл или игнорирует «правило пяти». Собственно пример и построен на том, что реализован конструктор копирования, а конструктор перемещения создан компилятором — классический пример применения этого правила.

Нельзя не заметить, что определённую оптимизацию, позволяющую избежать копирований временного объекта Widget, сделать всё-таки можно. Для этого мы вместо копирования продляем жизнь временному объекту (подробности), которая вернула функция doSomeVeryComplicatedThingWithSeveralArguments, а затем перемещаем его.


void someFunctionV3() {
    Widget&& complicatedThingResult =
        doSomeVeryComplicatedThingWithSeveralArguments(123, "hello");
    consume(std::move(complicatedThingResult));
}
нет особой разницы между этими вариантами:
Widget&& foo = someFunc();
Widget foo = someFunc();
Кроме крайне шаблонных вариантов, а-ля
std::forward
или универсальной ссылки вида auto&& foo = Foo(...);, где Foo() может возвращать результат либо по значению, либо по lvalue. По крайней мере в вашем примере код изменился только в том, что стал сложнее для прочтения начинающими плюсовиками.

Разницы нет в случае RVO/NRVO, но как вы же сами выше и продемонстрировали, есть ситуации, когда эти оптимизации не применяются. В случае же с rvalue ссылкой мы гарантируем отсутствие копирований временного объекта.

нет, не гарантируете. Код
Widget&& complicatedThingResult =
    doSomeVeryComplicatedThingWithSeveralArguments(...);
consume(complicatedThingResult); // без std::move
точно так же скомпилируется и в нем точно так же будет копирование, как и в варианте с не-ссылочной переменной.
Sign up to leave a comment.