关于c ++:C ++ 11 rvalues和移动语义混淆(return语句)

C++11 rvalues and move semantics confusion (return statement)

我试图理解R值引用并移动C++ 11的语义。

这些例子之间有什么区别,哪一个不做向量复制?

第一实例

1
2
3
4
5
6
7
std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

第二实例

1
2
3
4
5
6
7
std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

第三例

1
2
3
4
5
6
7
std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();


型第一个例子

1
2
3
4
5
6
7
std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

第一个示例返回一个被rval_ref捕获的临时变量。这个临时的生命周期将超过rval_ref的定义,您可以将它当作是按价值捕获的一样使用。这与以下内容非常相似:

1
const std::vector<int>& rval_ref = return_vector();

除了在我的重写中,你显然不能以非常量的方式使用rval_ref

第二个例子

1
2
3
4
5
6
7
std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

在第二个示例中,您创建了一个运行时错误。rval_ref现在在函数内部保存对已销毁的tmp的引用。如果幸运的话,这个代码会立即崩溃。

第三个例子

1
2
3
4
5
6
7
std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

第三个例子大致相当于第一个例子。tmp上的std::move是不必要的,实际上可以是一个性能问题,因为它会抑制返回值优化。

编写代码的最佳方法是:

最佳实践

1
2
3
4
5
6
7
std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

就像你在C++ 03中所做的一样。tmp在RETURN语句中被隐式地视为右值。它将通过返回值优化(不复制,不移动)返回,或者如果编译器决定它不能执行rvo,那么它将使用vector的move构造函数来执行返回。只有在不执行rvo的情况下,并且如果返回的类型没有move构造函数,复制构造函数才会用于返回。


它们都不会复制,但第二个是指被破坏的向量。命名的右值引用在常规代码中几乎不存在。你写的只是你如何在C++ 03中写一个拷贝。

1
2
3
4
5
6
7
std::vector<int> return_vector()
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

除了现在,矢量是移动的。在大多数情况下,类的用户不会处理它的右值引用。


简单的答案是,您应该为右值引用编写代码,就像常规引用代码一样,并且在99%的情况下,您应该在精神上对待它们。这包括有关返回引用的所有旧规则(即从不返回对局部变量的引用)。

除非您正在编写一个模板容器类,该类需要利用std::forward并能够编写一个采用lvalue或rvalue引用的通用函数,否则这或多或少是正确的。

move构造函数和move赋值的一大优点是,如果定义了它们,编译器可以在调用rvo(返回值优化)和nrvo(命名为返回值优化)失败的情况下使用它们。这对于高效地从方法中按值返回容器和字符串等昂贵的对象来说是非常巨大的。

现在,右值引用的有趣之处在于,您还可以将它们用作普通函数的参数。这允许您为const-reference(const-foo&other)和rvalue-reference(foo&other)编写重载的容器。即使参数太难用一个简单的构造函数调用传递,仍然可以做到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
std::vector vec;
for(int x=0; x<10; ++x)
{
    // automatically uses rvalue reference constructor if available
    // because MyCheapType is an unamed temporary variable
    vec.push_back(MyCheapType(0.f));
}


std::vector vec;
for(int x=0; x<10; ++x)
{
    MyExpensiveType temp(1.0, 3.0);
    temp.initSomeOtherFields(malloc(5000));

    // old way, passed via const reference, expensive copy
    vec.push_back(temp);

    // new way, passed via rvalue reference, cheap move
    // just don't use temp again,  not difficult in a loop like this though . . .
    vec.push_back(std::move(temp));
}

STL容器已经更新为几乎所有内容(散列键和值、向量插入等)都有移动重载,您将在其中看到最多的内容。

也可以将它们用于普通函数,如果只提供右值引用参数,则可以强制调用方创建对象并让函数进行移动。这不仅仅是一个很好的例子,但在我的渲染库中,我已经为所有加载的资源分配了一个字符串,这样就更容易看到调试器中每个对象所代表的内容。接口是这样的:

1
2
3
4
5
6
TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

它是一种"泄漏抽象"的形式,但允许我利用这个事实,我必须在大多数时候创建字符串,并避免对它进行另一次复制。这并不完全是高性能的代码,但它是一个很好的例子,说明了随着人们掌握了这一特性。此代码实际上要求变量要么是调用的临时变量,要么是调用的std::move:

1
2
// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

1
2
3
// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

1
2
3
4
// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

但这不会编译!

1
2
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);

不是答案本身,而是指导方针。在大多数情况下,声明本地T&&变量没有多大意义(正如您对std::vector&& rval_ref所做的那样)。在foo(T&&)类型的方法中仍然需要使用std::move()它们。还有一个问题已经提到,当您试图从函数返回这样的rval_ref时,您将获得标准引用,以破坏临时性的错误。

大多数情况下,我会采用以下模式:

1
2
3
4
5
6
// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

对于返回的临时对象,您不持有任何引用,因此可以避免(没有经验的)希望使用移动对象的程序员错误。

1
2
3
4
5
6
7
8
auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Either these just fail (assert/exception), or you won't get
// your expected results due to their clean state.
bRet.foo();
cRet.bar();

显然,在某些情况下(尽管非常罕见),函数确实返回一个T&&,它是对一个可以移动到对象中的非临时对象的引用。

关于rvo:这些机制通常都可以工作,编译器可以很好地避免复制,但是在返回路径不明显的情况下(例外情况,if条件决定了将返回的命名对象,可能还有其他两个条件),rref是您的救星(即使可能更昂贵)。


这些都不会做任何额外的复制。即使没有使用RVO,新的标准说,我相信在做退货的时候,移动结构最好是复制的。

我相信您的第二个示例会导致未定义的行为,因为您将返回对局部变量的引用。


正如第一个答案的注释中所提到的,return std::move(...);构造在返回局部变量以外的情况下可以产生差异。下面是一个可运行的示例,它记录了当返回带有或不带有std::move()的成员对象时所发生的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout <<"A copied
"
; }
  A(A&&) { std::cout <<"A moved
"
; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout <<"B C-value:"; return a; }
  operator A() & { std::cout <<"B L-value:"; return a; }
  operator A() && { std::cout <<"B R-value:"; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout <<"C C-value:"; return std::move(a); }
  operator A() & { std::cout <<"C L-value:"; return std::move(a); }
  operator A() && { std::cout <<"C R-value:"; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

大概,return std::move(some_member);只有在您实际想要移动特定的类成员时才有意义,例如,在class C表示短暂的适配器对象的情况下,其唯一目的是创建struct A的实例。

注意,即使class B对象是r值,struct A总是从class B中复制出来。这是因为编译器无法告诉class Bstruct A实例将不再使用。在class C中,编译器确实有来自std::move()的信息,这就是struct A被移动的原因,除非class C的实例是常量。