Можно ли написать код на С++, где мы по возможности полагаемся на оптимизацию возвращаемого значения (RVO), но не возвращаемся к семантике перемещения, если нет? Например, следующий код не может использовать RVO из-за условного, поэтому он копирует результат:
#include <iostream>
struct Foo {
Foo() {
std::cout << "constructor" << std::endl;
}
Foo(Foo && x) {
std::cout << "move" << std::endl;
}
Foo(Foo const & x) {
std::cout << "copy" << std::endl;
}
~Foo() {
std::cout << "destructor" << std::endl;
}
};
Foo f(bool b) {
Foo x;
Foo y;
return b ? x : y;
}
int main() {
Foo x(f(true));
std::cout << "fin" << std::endl;
}
Это дает
constructor
constructor
copy
destructor
destructor
fin
destructor
что имеет смысл. Теперь я могу заставить конструктор перемещения вызываться в приведенном выше коде, изменив строку
return b ? x : y;
к
return std::move(b ? x : y);
Это дает выход
constructor
constructor
move
destructor
destructor
fin
destructor
Однако мне не очень нравится называть std:: move напрямую.
Действительно, проблема в том, что я в ситуации, когда я абсолютно, положительно, не могу вызвать конструктор копирования, даже когда существует конструктор. В моем случае использования слишком много памяти для копирования, и хотя было бы неплохо просто удалить конструктор копирования, это не вариант для множества причин. В то же время я хотел бы вернуть эти объекты из функции и предпочел бы использовать RVO. Теперь я действительно не хочу помнить все нюансы RVO при кодировании и когда он применяется, когда он не применяется. В основном, я хочу, чтобы объект был возвращен, и я не хочу, чтобы вызвал конструктор копирования. Конечно, RVO лучше, но семантика перемещения прекрасна. Есть ли способ к RVO, когда это возможно, и семантика перемещения, если нет?
Изменить 1
Следующий question помог мне разобраться, что происходит. В основном, 12.8.32 стандартных состояний:
Когда критерии для исключения операции копирования выполняются или будут сэкономленные за тот факт, что исходный объект является параметром функции, и подлежащий копированию объект обозначается значением lvalue, перегрузкой разрешение для выбора конструктора для копии сначала выполняется как будто объект был обозначен rvalue. Если разрешение перегрузки сбой, или если тип первого параметра выбранного конструктор не является ссылкой rvalue на тип объектов (возможно cv-qualified), разрешение перегрузки выполняется снова, учитывая объект как lvalue. [Примечание. Это двухступенчатое разрешение перегрузки должно выполняться независимо от того, произойдет ли копирование. Это определяет вызывающий конструктор, если elision не выполняется, и выбранный конструктор должен быть доступен, даже если вызов опущены. -end note]
Хорошо, поэтому, чтобы выяснить, каковы критерии для копии elison, посмотрим на 12.8.31
в операторе return в функции с возвращаемым типом класса, когда выражение - это название энергонезависимого автоматического объекта (кроме параметр функции или catch-clause) с тем же самым cvunqualified типом в качестве возвращаемого типа функции операцию копирования/перемещения можно опустить построение автоматического объекта непосредственно в функции return Значение
Таким образом, если мы определим код для f как:
Foo f(bool b) {
Foo x;
Foo y;
if(b) return x;
return y;
}
Затем каждое из наших возвращаемых значений является автоматическим объектом, поэтому в 12.8.31 говорится, что он подходит для копирования elison. Это достигает 12.8.32, в котором говорится, что копия выполняется так, как если бы она была rvalue. Теперь RVO не происходит, потому что мы не знаем априори, какой путь взять, но конструктор перемещения вызван из-за требований в 12.8.32. Технически один конструктор перемещения избегается при копировании в x. В основном, при запуске мы получаем:
constructor
constructor
move
destructor
destructor
fin
destructor
Отключение elide на конструкторах генерирует:
constructor
constructor
move
destructor
destructor
move
destructor
fin
destructor
Теперь, скажем, мы вернемся к
Foo f(bool b) {
Foo x;
Foo y;
return b ? x : y;
}
Мы должны смотреть на семантику условного оператора в 5.16.4
Если второй и третий операнды являются значениями одного и того же значения категории и имеют один и тот же тип, результат такого типа и значения и это бит-поле, если второй или третий операнд является бит-поле, или если оба являются битовыми полями.
Поскольку оба x и y являются lvalues, условный оператор является lvalue, но не является автоматическим объектом. Таким образом, 12.8.32 не срабатывает, и мы возвращаем возвращаемое значение как lvalue, а не rvalue. Для этого требуется вызвать конструктор копирования. Следовательно, получаем
constructor
constructor
copy
destructor
destructor
fin
destructor
Теперь, поскольку условный оператор в этом случае в основном копирует категорию значений, это означает, что код
Foo f(bool b) {
return b ? Foo() : Foo();
}
вернет rvalue, так как обе ветки условного оператора являются значениями r. Мы видим это следующим образом:
constructor
fin
destructor
Если мы отключим elide на конструкторах, мы увидим движения
constructor
move
destructor
move
destructor
fin
destructor
В принципе, идея состоит в том, что если мы вернем rvalue, мы назовем конструктор перемещения. Если мы вернем lvalue, мы назовем конструктор копирования. Когда мы возвращаем энергонезависимый автоматический объект, тип которого соответствует типу возвращаемого типа, мы возвращаем значение rvalue. Если у нас есть достойный компилятор, эти копии и ходы могут быть отменены с помощью RVO. Однако, по крайней мере, мы знаем, какой конструктор вызывается в случае, если RVO не может быть применен.