C++ 11 rvalues和移动语义混淆(return语句)

Tar*_*ula 415 c++ c++-faq rvalue-reference move-semantics c++11

我试图理解rvalue引用并移动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();
Run Code Online (Sandbox Code Playgroud)

第二个例子

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();
Run Code Online (Sandbox Code Playgroud)

第三个例子

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();
Run Code Online (Sandbox Code Playgroud)

How*_*ant 543

第一个例子

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

std::vector<int> &&rval_ref = return_vector();
Run Code Online (Sandbox Code Playgroud)

第一个示例返回一个临时的,它被捕获rval_ref.那个临时的生命将超出rval_ref定义,你可以使用它,好像你已经按价值捕获它.这非常类似于以下内容:

const std::vector<int>& rval_ref = return_vector();
Run Code Online (Sandbox Code Playgroud)

除了在我的重写中,你显然不能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();
Run Code Online (Sandbox Code Playgroud)

在第二个示例中,您创建了一个运行时错误. 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();
Run Code Online (Sandbox Code Playgroud)

你的第三个例子大致相当于你的第一个例子.该std::movetmp是不必要的,而且实际上是一个性能pessimization,因为它会抑制返回值优化.

编码您正在做的事情的最佳方式是:

最佳实践

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

std::vector<int> rval_ref = return_vector();
Run Code Online (Sandbox Code Playgroud)

就像你在C++ 03中一样. tmp在return语句中隐式地将其视为rvalue.它将通过返回值优化(无复制,无移动)返回,或者如果编译器决定它不能执行RVO,那么它将使用向量的移动构造函数来执行返回.仅当未执行RVO时,并且如果返回的类型没有移动构造函数,则复制构造函数将用于返回.

  • 当你按值返回一个本地对象时,编译器将是RVO,并且本地的类型和函数的返回是相同的,并且都不是cv-qualified(不返回const类型).使用条件(:?)语句远离返回,因为它可以抑制RVO.不要将local包装在返回对本地的引用的其他函数中.只需`返回my_local;`.多个return语句都可以,并且不会禁止RVO. (61认同)
  • 有一点需要注意:当返回本地对象的_member_时,移动必须是显式的. (24认同)
  • "多个返回语句都可以,并且不会禁止RVO":仅当它们返回*相同的*变量时. (15认同)
  • @NoSenseEtAl:返回行上没有临时创建.`move`不会创造一个临时的.它将左值投射到x值,不做任何副本,不创造任何东西,不破坏任何东西.这个例子就像你用lvalue-reference返回并从返回行中删除`move`一样完全相同的情况:无论哪种方式,你都有一个对函数内部局部变量的悬空引用,并且已被破坏. (5认同)
  • @Deduplicator:你是对的.我没有像我预期的那样准确地说话.我的意思是多个return语句不禁止RVO编译器(即使它确实无法实现),因此返回表达式仍被视为右值. (5认同)
  • 只是一个问题:既然你在"最佳实践"部分中*命名了*变量(`tmp`),那么它就是NRVO,而不是RVO.这是两种不同的优化.除此之外,很棒的答案! (3认同)
  • @greenoldman:我错了.RVO可以使用const返回类型.这样做只是一个坏主意.如果RVO失败,则移动语义不会启动. (3认同)
  • @RobertDailey:表达式`return_vector()`是一个右值,因为函数按值返回一个对象.当该表达式用于在调用站点构造对象时,重载决策将选择移动构造函数(如果存在).如果对象已经构造,则重载决策将改为选择赋值运算符.由于rhs是一个rvalue,它将选择移动赋值运算符(如果存在). (2认同)
  • @gedamial:C++ 标准规定编译器编写者可以做出一些决定。其中之一是 RVO。在一组非常特定的情况下,允许但不要求编译器执行 RVO。决定是否执行 RVO 的是编译器,而不是您。“已经”有一些关于要求 RVO 的讨论,但目前尚未标准化。 (2认同)
  • @gedamial:好的,我已经试过了. (2认同)
  • @CiroSantilli新疆改造中心六四事件法轮功是的,您是正确的。而且,对分配/取消分配进行计数是一种评估性能的好方法,这也是正确的。 (2认同)

Pup*_*ppy 41

它们都不会复制,但第二个会引用一个被破坏的矢量.命名的右值引用几乎从不存在于常规代码中.你只需要在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();
Run Code Online (Sandbox Code Playgroud)

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

  • 我认为没有理由破坏你的建议.将局部右值引用变量绑定到右值是完全正确的.在这种情况下,临时对象的生命周期将延长到右值引用变量的生命周期. (4认同)
  • 只是澄清一点,因为我正在学习这一点。在这个新示例中,向量“tmp”没有“移动”到“rval_ref”中,而是使用 RVO(即复制省略)直接写入“rval_ref”。`std::move` 和复制省略之间有区别。`std::move` 可能仍然涉及一些要复制的数据;对于向量,实际上是在复制构造函数中构造了一个新向量并分配了数据,但仅通过复制指针(本质上)来复制大部分数据数组。复制省略可以 100% 避免所有复制。 (2认同)

Zon*_*ner 16

简单的答案是你应该为rvalue引用编写代码,就像你经常引用代码一样,你应该在99%的时间里对它们进行相同的处理.这包括有关返回引用的所有旧规则(即永远不会返回对局部变量的引用).

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

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

现在,rvalue引用让事情变得有趣,你也可以将它们用作普通函数的参数.这允许您编写具有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));
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

它是一种"漏洞抽象"的形式,但允许我利用我必须在大多数时间创建字符串的事实,并避免再次复制它.这不是完全高性能的代码,但是当人们掌握这个功能时,这是一个很好的例子.这段代码实际上要求变量是调用的临时变量,或调用std :: move:

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));
Run Code Online (Sandbox Code Playgroud)

要么

// 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));
Run Code Online (Sandbox Code Playgroud)

要么

// 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));
Run Code Online (Sandbox Code Playgroud)

但这不会编译!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);
Run Code Online (Sandbox Code Playgroud)