【问题标题】:C++11 rvalues and move semantics confusion (return statement)C++11 右值和移动语义混淆(return 语句)
【发布时间】:2011-06-26 13:54:00
【问题描述】:

我正在尝试理解右值引用并移动 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();

【问题讨论】:

  • 请永远不要通过引用返回局部变量。右值引用仍然是引用。
  • 这显然是故意的,以便理解示例之间的语义差异哈哈
  • @FredOverflow 老问题,但我花了一秒钟才理解您的评论。我认为#2 的问题是std::move() 是否创建了一个持久的“副本”。
  • @DavidLively std::move(expression) 不会创建任何东西,它只是将表达式转换为 xvalue。在评估 std::move(expression) 的过程中不会复制或移动任何对象。

标签: c++ c++11 move-semantics rvalue-reference c++-faq


【解决方案1】:

第一个例子

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

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

编写代码的最佳方式是:

最佳实践

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,那么它will use vector's move constructor to do the return。仅当不执行 RVO,并且返回的类型没有移动构造函数时,才会使用复制构造函数进行返回。

【讨论】:

  • 当你按值返回一个本地对象时,编译器会RVO,并且本地的类型和函数的返回是一样的,都不是cv限定的(不要返回const类型) .远离使用条件 (:?) 语句返回,因为它会抑制 RVO。不要将本地包装在返回对本地的引用的其他函数中。只需return my_local;。多个返回语句是可以的,不会抑制 RVO。
  • 有一个警告:当返回一个本地对象的成员时,移动必须是显式的。
  • @NoSenseEtAl:退货线路上没有临时创建。 move 不会创建临时文件。它将左值转换为 xvalue,不制作副本,不创建任何内容,不销毁任何内容。该示例与您通过左值引用返回并从返回行中删除move 完全相同的情况:无论哪种方式,您都有对函数内部局部变量的悬空引用并且已被破坏。跨度>
  • “多个返回语句都可以,不会抑制 RVO”:仅当它们返回相同的变量时。
  • @Deduplicator:你是对的。我说得没有我想的那么准确。我的意思是多个返回语句不会禁止编译器使用 RVO(即使它确实无法实现),因此返回表达式仍然被视为右值。
【解决方案2】:

它们都不会复制,但第二个将引用已破坏的向量。命名的右值引用几乎从不存在于常规代码中。你写它就像你在 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:它会破坏你的向量。它在破坏之前是否复制它并不重要。
  • 我看不出你提议的破坏有任何理由。将本地右值引用变量绑定到右值是非常好的。在这种情况下,临时对象的生命周期会延长到右值引用变量的生命周期。
  • 只是澄清一点,因为我正在学习这个。在这个新示例中,向量tmp 没有移动rval_ref,而是使用RVO(即复制省略)直接写入rval_refstd::move 和复制省略之间是有区别的。 std::move 可能还涉及到一些需要复制的数据;在向量的情况下,实际上在复制构造函数中构造了一个新向量并分配了数据,但是数据数组的大部分仅通过复制指针来复制(本质上)。复制省略可避免 100% 的所有副本。
  • @MarkLakata 这是 NRVO,不是 RVO。 NRVO 是可选的,即使在 C++17 中也是如此。如果未应用,则返回值和rval_ref 变量均使用std::vector 的移动构造函数构造。有/没有std::move都没有涉及到复制构造函数。在这种情况下,tmpreturn 语句中被视为 rvalue
【解决方案3】:

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

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

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

现在,右值引用的有趣之处在于,您还可以将它们用作普通函数的参数。这允许您编写具有 const 引用(const foo& other)和右值引用(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);

【讨论】:

    【解决方案4】:

    本身不是一个答案,而是一个指导方针。大多数情况下,声明本地变量T&amp;&amp; 没有多大意义(就像您对std::vector&lt;int&gt;&amp;&amp; rval_ref 所做的那样)。您仍然需要std::move() 他们才能在foo(T&amp;&amp;) 类型方法中使用。还有一个已经提到的问题,当你尝试从函数中返回这样的rval_ref 时,你会得到标准的reference-to-destroyed-temporary-fiasco。

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

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

    显然,在某些情况下(尽管相当罕见),函数真正返回 T&amp;&amp;,这是对您可以移动到对象中的非临时对象的引用。

    关于 RVO:这些机制通常有效,编译器可以很好地避免复制,但在返回路径不明显的情况下(例外,if 条件确定您将返回的命名对象,并且可能结合其他)rrefs 是您的救世主(即使可能更昂贵)。

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

        【解决方案6】:

        正如第一个答案的 cmets 中已经提到的,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 的实例。

        注意struct A 总是复制class B,即使class B 对象是一个R 值。这是因为编译器无法判断class Bstruct A 实例将不再被使用。在class C 中,编译器确实从std::move() 获得此信息,这就是struct A移动 的原因,除非class C 的实例是常量。

        【讨论】:

          猜你喜欢
          • 2012-12-10
          • 1970-01-01
          • 2013-02-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多