【问题标题】:When the move constructor is actually called if we have (N)RVO?如果我们有 (N)RVO,什么时候实际调用移动构造函数?
【发布时间】:2018-10-05 01:22:10
【问题描述】:

我从关于 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 默认执行其优化工作,我为什么要关心实现移动构造函数?

【问题讨论】:

  • 据我所知,C++17 保证了标准中的移动省略,即使它是仅复制类型。

标签: c++ c++11 move-semantics rvo nrvo


【解决方案1】:

首先,您可能应该确保Foo 跟在rule of three/five 之后并且具有移动/复制赋值运算符。 it is good practice 表示移动构造函数和移动赋值运算符为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"; }
};

在大多数情况下,您可以遵循rule of zero 并且实际上不需要定义任何这些特殊成员函数,编译器会为您创建它们,但它对此很有用。

(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));
}

Output:

Constructed
Move-constructed
Destructed
Destructed

用于标准库容器

移动构造函数的一个非常有用的例子是如果你有一个std::vector&lt;Foo&gt;。当您将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());
}

Output:

-- 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());
}

Output:

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);

Output:

Constructed
Constructed
Move-constructed
Destructed
Destructed
Destructed

或者如果返回变量也是函数参数:

Foo appendToFoo(Foo foo) {

  // append to foo...

  return foo;
}

int main() { 
  Foo f1;
  Foo f2 = appendToFoo(f1);
}

Output:

Constructed
Copy-constructed
Move-constructed
Destructed
Destructed
Destructed

优化右值的设置器

移动赋值运算符的一种情况是,如果您想优化右值的设置器。假设您有一个包含FooFooHolder,并且您想要一个setFoo 成员函数。然后,如果你想同时优化左值和右值,你应该有两个重载。一个采用对 const 的引用,另一个采用右值引用:

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
}

Output:

Constructed
Constructed
Copy-assigned  <-- setFoo with lvalue
Constructed
Move-assigned  <-- setFoo with rvalue
Destructed
Destructed
Destructed

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-12-12
    • 2013-01-22
    • 1970-01-01
    • 2011-03-17
    • 2011-09-27
    • 1970-01-01
    • 2013-06-21
    相关资源
    最近更新 更多