6

例如:

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::movepass-by-value. 编译器是否仍然可以检查变量没有进行更多更改并消除不必要的副本?

rvalue-reference方法仅使优化版本“隐式”,例如f(T()),并要求用户显式指定其他情况,例如,如果用户未完成实例,则f(std::move(t))需要显式制作副本。那么,在这种优化方面,方法被认为是好的吗?f(T(t));trvalue-reference

4

3 回答 3

5

这绝对不一样。For onceT &&只能绑定到右值,while可以T const &同时绑定到右值和左值。其次,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 指出的那样,正确的习惯用法实际上是按值传递

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

int main()
{
    std::string a("hello");
    foo(std::move(a));
}
于 2013-03-25T11:55:36.923 回答
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用户甚至不知道的自由移动优化..

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

它不是等价的,它是更少的工作,因为只有“指针”被传输,根本没有调用任何 c'tors,既没有移动也没有其他任何东西

于 2013-03-25T11:44:06.203 回答
1

获取const左值引用和获取右值引用是两件不同的事情。

相似之处:

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

差异:

  • 左值引用将const绑定到任何东西(左值或右值)。右值引用只会绑定到非右值const- 受到更多限制。

  • const函数内部的参数是左值引用时不能修改。当它是一个右值引用时,它可以被修改(因为它是 non- const)。

让我们看一些例子:

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

    1. 传递左值:

      T t; f(t);
      

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

    2. 传递一个右值:

      f(T());
      

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

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

  2. 取一个右值引用:`void f(T&& t);

    1. 传递左值:

      T t;
      f(t);
      

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

    2. 传递一个右值:

      f(T());
      

      这很好,因为右值引用可以绑定到右值。函数内部的引用t将引用由T().

现在让我们考虑std::move。首先要做的事情:std::move实际上并没有移动任何东西。这个想法是你给它一个左值,然后它把它变成一个右值。这就是它所做的一切。所以现在,如果你f采用右值引用,你可以这样做:

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

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

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

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

为什么这对函数有用?好吧,只要你给它一个左值(或const右值),就会调用第一个,而只要你给它一个可修改的右值,就会调用第二个。接收到一个右值通常意味着你得到了一个临时对象,这是个好消息,因为这意味着你可以破坏它的内部并根据你知道它不会存在太久的事实来执行优化。

因此,拥有这对函数可以让您在知道自己得到一个临时对象时进行优化。

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

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

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

当然,传递的对象并不总是临时对象。如上所示,您可以使用std::move将左值转换为右值。然后它似乎是该函数的临时对象。使用std::move基本上是说“我允许您将此对象视为临时对象。” 它是否真的被移动是无关紧要的。

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

void f(T t);

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

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

至于不同的参数类型是否会导致相同的代码——这完全是一个不同的问题。编译器在as-if 规则下运行。这仅仅意味着只要程序按照标准的要求运行,编译器就可以发出它喜欢的任何代码。因此,如果这些函数碰巧做了完全相同的事情,它们可能会发出相同的代码。或者他们可能不会。但是,如果您的函数采用 const 左值引用和右值引用正在做同样的事情,那么这是一个不好的迹象。

于 2013-03-25T13:43:27.937 回答