Когда на самом деле вызывается конструктор перемещения, если у нас есть (N)RVO?
Я понял из нескольких вопросов здесь о SO, что (N)RVO предотвращает вызов конструктора перемещения, когда объект возвращается по значению. Классический пример:
struct Foo {
Foo() { std::cout << "Constructed\n"; }
Foo(const Foo &) { std::cout << "Copy-constructed\n"; }
Foo(Foo &&) { std::cout << "Move-constructed\n"; }
~Foo() { std::cout << "Destructed\n"; }
};
Foo makeFoo() {
return Foo();
}
int main() {
Foo foo = makeFoo(); // Move-constructor would be called here without (N)RVO
}
Выход с (N)RVO включен:
Constructed
Destructed
Так в каких случаях будет вызываться конструктор перемещения, независимо от наличия (N)RVO? Не могли бы вы привести несколько примеров? Другими словами: почему я должен заботиться о реализации конструктора перемещения, если (N)RVO по умолчанию выполняет свою работу по оптимизации?
1 ответ
Во-первых, вы должны убедиться, что Foo
следует правилу три / пять и имеет операторы перемещения / копирования. И хорошей практикой для оператора перемещения и назначения перемещения является noexcept
:
struct Foo {
Foo() { std::cout << "Constructed\n"; }
Foo(const Foo &) { std::cout << "Copy-constructed\n"; }
Foo& operator=(const Foo&) { std::cout << "Copy-assigned\n"; return *this; }
Foo(Foo &&) noexcept { std::cout << "Move-constructed\n"; }
Foo& operator=(Foo &&) noexcept { std::cout << "Move-assigned\n"; return *this; }
~Foo() { std::cout << "Destructed\n"; }
};
В большинстве случаев вы можете следовать правилу нуля и на самом деле не нужно определять какие-либо из этих специальных функций-членов, компилятор создаст их для вас, но это полезно для этой цели.
(N) RVO только для возвращаемых значений функции. Это не относится, например, к параметрам функции. Конечно, компилятор может применять любые оптимизации, которые ему нравятся, в соответствии с правилом "как будто", поэтому мы должны быть осторожны при создании тривиальных примеров.
Параметры функции
Есть много случаев, когда будет вызываться конструктор перемещения или оператор присваивания. Но простой случай, если вы используете std::move
передать владение функции, которая принимает параметр по значению или по rvalue-reference:
void takeFoo(Foo foo) {
// use foo...
}
int main() {
Foo foo = makeFoo();
// set data on foo...
takeFoo(std::move(foo));
}
Constructed
Move-constructed
Destructed
Destructed
Для использования в стандартных контейнерах библиотеки
Очень полезный случай для Move-конструктора, если у вас есть std::vector<Foo>
, Как и ты push_back
объекты в контейнер, которые ему иногда приходится перераспределять и перемещать все существующие объекты в новую память. Если есть допустимый Move-конструктор, доступный на Foo
он будет использовать его вместо копирования:
int main() {
std::vector<Foo> v;
std::cout << "-- push_back 1 --\n";
v.push_back(makeFoo());
std::cout << "-- push_back 2 --\n";
v.push_back(makeFoo());
}
-- push_back 1 --
Constructed
Move-constructed <-- move new foo into container
Destructed
-- push_back 2 --
Constructed
Move-constructed <-- move existing foo to new memory
Move-constructed <-- move new foo into container
Destructed
Destructed
Destructed
Destructed
Списки инициализатора элементов конструктора
Я считаю, что конструкторы перемещения полезны в списках инициализаторов членов конструктора. Скажи, у тебя есть класс FooHolder
который содержит Foo
, Затем вы можете определить конструктор, который принимает Foo
by-value и перемещает его в переменную-член:
class FooHolder {
Foo foo_;
public:
FooHolder(Foo foo) : foo_(std::move(foo)) {}
};
int main() {
FooHolder fooHolder(makeFoo());
}
Constructed
Move-constructed
Destructed
Destructed
Это хорошо, потому что позволяет мне определить конструктор, который принимает lvalue или rvalues без ненужных копий.
Случаи поражения NVRO
RVO всегда применяется, но есть случаи, которые побеждают NVRO. Например, если у вас есть две именованные переменные и выбор возвращаемой переменной неизвестен во время компиляции:
Foo makeFoo(double value) {
Foo f1;
Foo f2;
if (value > 0.5)
return f1;
return f2;
}
Foo foo = makeFoo(value);
Constructed
Constructed
Move-constructed
Destructed
Destructed
Destructed
Или, если возвращаемая переменная также является параметром функции:
Foo appendToFoo(Foo foo) {
// append to foo...
return foo;
}
int main() {
Foo f1;
Foo f2 = appendToFoo(f1);
}
Constructed
Copy-constructed
Move-constructed
Destructed
Destructed
Destructed
Оптимизация сеттеров для значений
Один случай для оператора присваивания перемещения - это если вы хотите оптимизировать установщик для значений r. Скажи, что у тебя есть FooHolder
который содержит Foo
и ты хочешь setFoo
функция-член. Тогда, если вы хотите оптимизировать как lvalues, так и rvalues, у вас должно быть две перегрузки. Один, который принимает ссылку на const, а другой - на rvalue-ссылку:
class FooHolder {
Foo foo_;
public:
void setFoo(const Foo& foo) { foo_ = foo; }
void setFoo(Foo&& foo) { foo_ = std::move(foo); }
};
int main() {
FooHolder fooHolder;
Foo f;
fooHolder.setFoo(f); // lvalue
fooHolder.setFoo(makeFoo()); // rvalue
}
Constructed
Constructed
Copy-assigned <-- setFoo with lvalue
Constructed
Move-assigned <-- setFoo with rvalue
Destructed
Destructed
Destructed