如果我们有 (N)RVO,当实际调用移动构造函数时?

When the move constructor is actually called if we have (N)RVO?

本文关键字:调用 移动 构造函数 我们有 RVO 如果      更新时间:2023-10-16

我从SO的几个问题中了解到,当按值返回对象时,(N)RVO会阻止调用移动构造函数。经典示例:

struct Foo {
Foo()            { std::cout << "Constructedn"; }
Foo(const Foo &) { std::cout << "Copy-constructedn"; }
Foo(Foo &&)      { std::cout << "Move-constructedn"; }
~Foo()           { std::cout << "Destructedn"; }
};
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 默认执行其优化工作,我为什么要关心实现移动构造函数?

首先,您可能应该确保Foo遵循三/五的规则,并具有移动/复制赋值运算符。移动构造函数和移动赋值运算符的良好做法是noexcept

struct Foo {
Foo()                           { std::cout << "Constructedn"; }
Foo(const Foo &)                { std::cout << "Copy-constructedn"; }
Foo& operator=(const Foo&)      { std::cout << "Copy-assignedn"; return *this; }
Foo(Foo &&)            noexcept { std::cout << "Move-constructedn"; }
Foo& operator=(Foo &&) noexcept { std::cout << "Move-assignedn"; return *this; }
~Foo()                    { std::cout << "Destructedn"; }
};

在大多数情况下,您可以遵循零规则,实际上不需要定义任何这些特殊成员函数,编译器将为您创建它们,但它对于此目的很有用。

(N)RVO 仅用于函数返回值。例如,它不适用于函数参数。当然,编译器可以在"as-if"规则下应用它喜欢的任何优化,因此我们在制作琐碎的示例时必须小心。

函数参数

在许多情况下,将调用移动构造函数或移动赋值运算符。但一个简单的情况是,如果您使用std::move将所有权转移到接受参数按值或按右值引用的函数:

void takeFoo(Foo foo) {
// use foo...
}
int main() { 
Foo foo = makeFoo();
// set data on foo...
takeFoo(std::move(foo));
}

输出:

Constructed
Move-constructed
Destructed
Destructed

用于标准磁带库容器

对于移动构造函数来说,一个非常有用的情况是,如果你有一个std::vector<Foo>。当您将对象push_back到容器中时,它偶尔需要重新分配所有现有对象并将其移动到新内存中。如果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

构造函数成员初始值设定项列表

我发现移动构造函数在构造函数成员初始值设定项列表中很有用。假设您有一个包含Foo的类FooHolder。然后,您可以定义一个构造函数,该构造函数采用按值Foo并将其移动到成员变量中:

class FooHolder {
Foo foo_;
public:
FooHolder(Foo foo) : foo_(std::move(foo)) {} 
};
int main() { 
FooHolder fooHolder(makeFoo());
}

输出:

Constructed
Move-constructed
Destructed
Destructed

这很好,因为它允许我定义一个接受左值或右值的构造函数,而无需不必要的副本。

击败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

优化右值的二传手

移动赋值运算符的一种情况是,如果要优化右值的二库器。假设您有一个包含FooFooHolder,并且您需要一个setFoo成员函数。然后,如果要针对左值和右值进行优化,则应有两个重载。一个采用对常量的引用,另一个采用右值引用:

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