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
?
- Привести к rvalue с помощью
А еще где-то неподалеку perfect forwarding...
В "return expr;" expr всегда является rvalue, то есть будет вызван конструктор перемещения, если он есть. Поэтому вектор можно спокойно возвращать по значению (без std::move)
Более того, если возвращать return std::move(expr);
то rvo не будет, а будет конструктор перемещения всегда
Но с учётом того, что возвращаемый объект модифицируется в процессе работы фукнции и, если функция раньше времени упадёт, то может остаться неоделанный объект, если исключение неправильн обработано будет.
Для начала стоит пользоваться C++20, у которого больше свободы в использовании неявного перемещения.
До него не каждый return перемещал.
https://oleksandrkvl.github.io/2021/04/02/cpp-20-overview.html#more-impl-moves
Насколько мне известно, основная рекомендация: возвращать по значению и положиться на RVO/NVRO. Может ли тут быть ситуация, когда это не сработает?Скажем, в таком случае RVO сработает:
std::string foo = ...;
return foo;
а в таком нет:std::string foo = ...;
std::string bar = ...;
return cond ? foo : bar;
поэтому во втором случае лучше добавлять std::move. Подробнее можно почитать у Майерса, он хорошо эти случаи разбирает.И, конечно же, вы ожидаете, что, согласно правилам С++, этот конструктор будет вызван всякий раз когда объект вашего класса копируется. Но если компиляторы будут непредсказуемо удалять копирование, тем самым удаляя пары вызовов копирующего/перемещающего конструктора и деструктора они могут разрушить всю логику вашего кода.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 ссылкой мы гарантируем отсутствие копирований временного объекта.
Печальная правда о пропуске копий в C++