C ++ 11右值和移动语义混淆(返回语句)


435

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

这些示例之间有什么区别,哪些将不进行矢量复制?

第一个例子

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

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

第二个例子

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

第三个例子

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

50
请永远不要通过引用返回局部变量。右值引用仍然是引用。
fredoverflow

63
这显然是有意的,目的是为了理解示例之间的语义差异
塔兰图拉毒蛛

@FredOverflow旧问题,但花了我一秒钟时间才能理解您的评论。我认为#2的问题是是否std::move()创建了持久的“副本”。
3Dave

5
@DavidLively std::move(expression)不创建任何内容,只是将表达式强制转换为xvalue。在评估过程中,不会复制或移动任何对象std::move(expression)
fredoverflow

Answers:


562

第一个例子

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定义范围,您可以像按值捕获它一样使用它。这与以下内容非常相似:

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

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

第二个例子

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内部被破坏的引用。运气好的话,此代码将立即崩溃。

第三个例子

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

您的第三个示例与您的第一个示例大致相同。该std::moveon tmp是不必要的,实际上可能是性能悲观,因为它将抑制返回值的优化。

编写正在执行的操作的最佳方法是:

最佳实践

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构造函数执行return。仅当不执行RVO且返回的类型没有移动构造函数时,才将复制构造函数用于返回。


64
当您按值返回本地对象,并且本地类型和函数的返回相同,并且都不是cv限定的(不返回const类型)时,编译器将RVO。避免返回条件(:?)语句,因为它可能会抑制RVO。不要将本地包装在其他返回本地引用的函数中。只是return my_local;。可以使用多个return语句,并且不会禁止RVO。
Howard Hinnant

27
需要注意的是:返回本地对象的成员时,此举必须明确。
boycy

5
@NoSenseEtAl:在返回行上没有创建任何临时文件。 move不会创建临时文件。它将左值转换为x值,不创建副本,不创建任何内容,不破坏任何内容。该示例与通过lvalue-reference返回并将其move从返回行中删除的情况完全相同:两种方式都可以使函数内部的局部变量悬空引用,并且该引用已被销毁。
2013年

15
“可以使用多个return语句,并且不会禁止RVO”:仅当它们返回相同的变量时。
Deduplicator 2014年

5
@Deduplicator:您是正确的。我的讲话不如预期的那么准确。我的意思是,多个return语句并不禁止RVO编译器(即使确实使它无法实现),因此return表达式仍被视为右值。
Howard Hinnant 2014年

42

它们都不会复制,但是第二个将引用被破坏的向量。在常规代码中,几乎没有命名的右值引用。您可以像在C ++ 03中编写副本一样来编写它。

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

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

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


您真的确定第三个示例将进行矢量复制吗?
塔兰图拉毒蛛

@Tarantula:它会破坏您的向量。在中断之前是否复制它并不重要。
小狗

4
我认为您提出的建议不存在任何理由。将本地右值引用变量绑定到右值是完全可以的。在这种情况下,临时对象的生存期将扩展到右值引用变量的生存期。
fredoverflow

1
请澄清一下,因为我正在学习。在该新的例子中,矢量tmp移动rval_ref,但直接写入rval_ref使用RVO(即复制省略)。std::move和复制省略之间是有区别的。A std::move可能仍包含一些要复制的数据;对于向量,实际上是在复制构造函数中构造了一个新向量,并分配了数据,但是数据数组的大部分仅通过复制指针来复制(本质上)。省略副本避免了所有副本的100%。
马克·拉卡塔

@MarkLakata这是NRVO,不是RVO。NRVO是可选的,即使在C ++ 17中也是如此。如果未应用,rval_ref则使用的move构造函数构造返回值和变量std::vector。没有/没有拷贝复制构造器std::move。在这种情况下,在语句中tmp被视为右值return
丹尼尔·兰格'18

16

简单的答案是,您应该像为常规引用代码一样为右值引用编写代码,并且应该在99%的时间里对它们进行相同的心理处理。这包括有关返回引用的所有旧规则(即,永不返回对局部变量的引用)。

除非您编写的模板容器类需要利用std :: forward并能够编写采用左值或右值引用的泛型函数,否则这或多或少都是正确的。

move构造函数和move分配的一大优点是,如果定义了它们,则编译器可以在无法调用RVO(返回值优化)和NRVO(命名为返回值优化)的情况下使用它们。对于通过方法有效地按值返回昂贵的对象(例如容器和字符串)而言,这是巨大的。

现在,右值引用使事情变得有趣,您还可以将它们用作常规函数的参数。这使您可以编写对const引用(const foo&other)和rvalue引用(foo && other)都具有重载的容器。即使参数太笨拙以至于不能仅通过构造函数调用来传递,也仍然可以做到:

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容器已更新,几乎所有内容(哈希键和值,向量插入等)都具有移动重载,在这里您将最常看到它们。

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

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的变量:

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

要么

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

要么

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

但这不会编译!

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

3

本身不是答案,而是指南。在大多数情况下,声明局部T&&变量没有太多意义(就像您对进行的操作一样std::vector<int>&& rval_ref)。您仍然必须std::move()foo(T&&)类型方法中使用它们。还有一个已经提到的问题,当您尝试rval_ref从函数中返回此类代码时,您将获得标准的销毁临时性惨败参考。

大多数时候,我会采用以下模式:

// Declarations
A a(B&&, C&&);
B b();
C c();

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

您无需保留对返回的临时对象的任何引用,从而避免了(无经验的)程序员希望使用移动对象的错误。

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

显然,在某些情况下(尽管很少见),一个函数真正返回a T&&,这是对非临时对象的引用,您可以将其移入对象。

关于RVO:这些机制通常可以正常工作,并且编译器可以很好地避免复制,但是在返回路径不明显的情况if下(例外情况,确定要返回的命名对象的条件,以及可能耦合的其他对象),rref是您的救星(即使可能更多)昂贵)。


2

这些都不会做任何额外的复制。即使不使用RVO,新标准也表示,我相信在进行退货时,最好采用移动结构来复制。

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


1

正如在第一个答案的注释中已经提到的那样,该return std::move(...);构造可以在返回局部变量以外的情况下有所作为。这是一个可运行的示例,其中记录了在返回带有和不带有成员的成员对象时发生的情况std::move()

#include <iostream>
#include <utility>

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

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

请注意,即使对象是R值,也struct A总是会从中复制出来。这是因为编译器无法告知将不再使用的实例。在中,编译器确实有这个信息,这是为什么被感动,除非实例是恒定的。class Bclass Bclass Bstruct Aclass Cstd::move()struct Aclass C

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.