std :: move与编译器优化

时间:2013-03-25 11:40:27

标签: c++ compiler-optimization move-semantics

例如:

void f(T&& t); // probably making a copy of t

void g() 
{
    T t;
    // do something with t
    f(std::move(t));
    // probably something else not using "t"
}

在这种情况下,void f(T const& t)是否等效,因为任何好的编译器都会产生相同的代码?如果这很重要,我对> = VC10和> = GCC 4.6感兴趣。

修改

根据答案,我想详细说明一下这个问题:

比较rvalue-referencepass-by-value方法,很容易忘记在std::move中使用pass-by-value。编译器是否仍然可以检查是否不再对变量进行更改并消除不必要的副本?

rvalue-reference方法仅使优化版本“隐式”,例如f(T()),并要求用户明确指定其他情况,例如f(std::move(t)),或者如果用户未完成f(T(t));实例,则明确指定副本t。那么,在这种与优化有关的问题中,rvalue-reference方法被认为是好的吗?

3 个答案:

答案 0 :(得分:5)

绝对不一样。一旦T &&只能 绑定到rvalues,而T const &可以绑定到rvalues和lvalues。其次,T const &不允许任何移动优化。如果您“可能想要复制t”,那么T &&可让您实际制作t的移动副本,这可能更有效。

示例:

void foo(std::string const & s) { std::string local(s); /* ... */ }

int main()
{
    std::string a("hello");
    foo(a);
}

在此代码中,包含"hello"的字符串缓冲区必须存在两次,一次位于main的正文中,另一次位于foo正文中。相比之下,如果您使用了右值引用和std::move(a),则可以“移动”相同的字符串缓冲区,并且只需要一次分配和填充。

正如@Alon指出的那样,正确的习语实际上是passing-by-value

void foo(std::string local) { /* same as above */ }

int main()
{
    std::string a("hello");
    foo(std::move(a));
}

答案 1 :(得分:2)

嗯,这取决于f对t的作用,如果它创建了它的副本,那么我甚至会这么做:

void f(T t) // probably making a copy of t
{
    m_newT = std::move(t); // save it to a member or take the resources if it is a c'tor..
}

void g() 
{
    T t;
    // do something with t
    f(std::move(t));
    // probably something else not using "t"
}

然后你允许移动c'tors优化发生,你在任何情况下都采用't'资源,如果它被'移动'到你的函数,那么你甚至获得了将它移动到函数的非副本,如果没有移动那么你可能只需要一份副本

现在,如果稍后您将拥有以下代码:

f(T());

然后ta da,自由移动优化,而f用户甚至不知道..

注意引用:“在这种情况下是无效的f(T const& t)等价物,因为任何好的编译器都会产生相同的代码吗?”

它不是平等的,它是无用的,因为只有“指针”被转移,根本没有被调用,既不移动也不移动任何东西

答案 2 :(得分:1)

采用const左值参考并采用左值参考是两回事。

相似点:

  • 两者都不会导致复制或移动,因为它们都是引用。引用只引用一个对象,它不会以任何方式复制/移动它。

的差异:

  • const左值引用将绑定到任何值(左值或右值)。右值引用只会绑定到非const右值 - 更有限。

  • const左值参考时,函数内部的参数无法修改。它可以在它是右值参考时修改(因为它不是const)。

让我们看看一些例子:

  1. 采用const左值参考:void f(const T& t);

    1. 传递左值:

      T t; f(t);
      

      这里,t是左值表达式,因为它是对象的名称。 const左值引用可以绑定到任何内容,因此t将很乐意通过引用传递。没有任何东西被复制,什么也没有被移动。

    2. 传递右值:

      f(T());
      

      这里,T()是一个右值表达式,因为它创建了一个临时对象。同样,const左值引用可以绑定到任何东西,所以这没关系。没有任何东西被复制,什么也没有被移动。

    3. 在这两种情况下,函数内部的t都是对传入的对象的引用。它不能被引用修改为const

      < / LI>
    4. 采用右值参考:`void f(T&amp;&amp; t);

      1. 传递左值:

        T t;
        f(t);
        

        这会给你一个编译器错误。右值引用不会绑定到左值。

      2. 传递右值:

        f(T());
        

        这很好,因为右值引用可以绑定到右值。函数内的引用t将引用由T()创建的临时对象。

    5. 现在让我们考虑std::move。首先要做的事情是:std::move实际上并没有移动任何东西。这个想法是你给它一个左值并将它变成一个右值。这就是全部。现在,如果您的f采用左值引用,则可以执行以下操作:

      T t;
      f(std::move(t));
      

      这是有效的,因为虽然t是左值,但std::move(t)是左值。现在rvalue引用可以绑定它。

      那你为什么要采用右值参考参数呢?事实上,除了定义移动构造函数和赋值运算符之外,您不应该经常这样做。每当你定义一个带右值引用的函数时,你几乎肯定想要给出const左值引用过载。它们应该几乎总是成对出现:

      void f(const T&);
      void f(T&&);
      

      为什么这对功能有用?好吧,每当你给它一个左值(或const右值)时,第一个就会被调用,只要你给它一个可修改的右值,就会调用第二个。接收右值通常意味着你已经获得了一个临时对象,这是一个好消息,因为这意味着你可以根据你知道它不会存在的事实来破坏它的内部并进行优化更长的时间。

      因此,拥有这对功能可让您在知道自己获得临时对象时进行优化。

      这对功能有一个非常常见的例子:复制和移动构造函数。它们通常定义如下:

      T::T(const T&); // Copy constructor
      T::T(T&&); // Move constructor
      

      因此,移动构造函数实际上只是一个复制构造函数,它在接收临时对象时进行了优化。

      当然,传递的对象并不总是一个临时对象。如上所示,您可以使用std::move将左值变为右值。然后它出现成为该函数的临时对象。使用std::move基本上说&#34;我允许您将此对象视为临时对象。&#34;它实际上是否被移动是无关紧要的。

      然而,除了编写复制构造函数和移动构造函数之外,您最好有充分的理由使用这对函数。如果您正在编写一个接受一个对象的函数,并且无论它是否是一个临时对象,它将与它完全相同,只需按值获取该对象即可!考虑:

      void f(T t);
      
      T t;
      f(t);
      f(T());
      

      在第一次调用f时,我们传递左值。这将复制到函数中。在第二次调用f时,我们传递一个右值。该对象将移动到函数中。请参阅 - 我们甚至不需要使用右值引用来使对象有效移动。我们刚刚接受了价值!为什么?因为根据表达式是左值还是右值来选择用于进行复制/移动的构造函数。只需让复制/移动构造函数完成它们的工作。

      关于不同的参数类型是否会产生相同的代码 - 这完全是一个不同的问题。编译器在 as-if规则下运行。这只是意味着只要程序的行为符合标准规定,编译器就可以发出它喜欢的任何代码。因此,如果它们碰巧完全相同,那么函数可能会发出相同的代码。或者他们可能没有。但是,如果你的函数采用const左值引用并且rvalue引用做同样的事情,这是一个不好的迹象。