21

我认为我对右值引用不太了解。为什么以下无法编译(VS2012)并出现错误'foo' : cannot convert parameter 1 from 'int' to 'int &&'

void foo(int &&) {}
void bar(int &&x) { foo(x); };

我会假设int &&当从 bar 传递到 foo 时类型会被保留。为什么它会int在函数体内转化为一次?

我知道答案是使用std::forward

void bar(int &&x) { foo(std::forward<int>(x)); }

所以也许我只是不清楚为什么。(另外,为什么不std::move呢?)

4

5 回答 5

15

我一直记得左值是一个有名字或可以寻址的值。由于 x 有名称,因此它作为左值传递。引用右值的目的是允许函数以它认为合适的任何方式完全破坏值。如果我们在您的示例中通过引用传递 x ,那么我们无法知道这样做是否安全:

void foo(int &&) {}
void bar(int &&x) { 
    foo(x); 
    x.DoSomething();   // what could x be?
};

Doingfoo(std::move(x));明确地告诉编译器你已经完成了 x 并且不再需要它。如果没有这一举措,现有代码可能会发生坏事。是std::move一种保障。

std::forward用于模板中的完美转发。

于 2012-09-26T20:03:13.890 回答
11

为什么它会int在函数体内转化为一次?

它没有;它仍然是对rvalue的引用。

当名称出现在表达式中时,它就是一个左值——即使它恰好是对右值的引用。如果表达式需要,它可以转换为右值(即如果需要它的值);但它不能绑定到右值引用。

正如您所说,为了将其绑定到另一个rvalue引用,您必须将其显式转换为未命名的rvaluestd::forward并且std::move是方便的方法。

另外,为什么不std::move呢?

为什么不呢?这比 , 更有意义std::forward,它适用于不知道参数是否为引用的模板。

于 2012-09-26T17:06:11.530 回答
9

这是“无名规则”。里面barx有一个名字x…… 所以它现在是一个左值。将某些内容作为右值引用传递给函数不会使其成为函数内部的右值。

如果你不明白为什么一定是这样,问问你自己——返回x之后是什么foo?(记住,foo可以自由移动x。)

于 2012-09-26T17:00:02.560 回答
2

rvaluelvalue表达式的类别。

右值引用左值引用是引用的类别。

在声明中T x&& = <initializer expression>,变量 x 的类型为 T&&,并且它可以绑定到作为右值表达式的表达式 (the) 。因此, T&& 被命名为右值引用类型,因为它引用了一个右值表达式

在声明中T x& = <initializer expression>,变量 x 的类型为 T&,它可以绑定到一个表达式 (the ),该表达式是一个左值表达式 (++)。因此,T& 被命名为左值引用类型,因为它可以引用左值表达式

因此,在 C++ 中,重要的是要区分出现在声明中的实体的命名和该名称出现在表达式中的时间。

当一个名字出现在一个表达式中时foo(x),这个名字x本身就是一个表达式,称为 id-expression。根据定义,id-expression 始终是左值表达式,并且左值表达式不能绑定到右值引用

于 2018-03-08T18:46:36.793 回答
0

在谈论右值引用时,区分引用生命周期中两个不相关的关键步骤是很重要的——绑定和值语义。

这里的绑定是指调用函数时值与参数类型匹配的确切方式。

例如,如果您有函数重载:

void foo(int a) {}
void foo(int&& a) {}

然后在调用时foo(x),选择适当的重载的行为涉及将值绑定x到 的参数foo

右值引用仅与绑定语义有关。

在两个 foo 函数的主体内,该变量a充当常规左值。也就是说,如果我们这样重写第二个函数:

void foo(int&& a) {
    foo(a);
} 

那么直觉上这应该会导致堆栈溢出。但事实并非如此——右值引用都是关于绑定的,而不是关于值语义的。由于a是函数体内的常规左值,因此foo(int)将在该点调用第一个重载并且不会发生堆栈溢出。只有当我们显式更改 的值类型时才会发生堆栈溢出a,例如通过使用std::move

void foo(int&& a) {
    foo(std::move(a));
}

此时,由于值语义发生了变化,将发生堆栈溢出。

在我看来,这是右值引用最令人困惑的特性——类型在绑定期间和之后的工作方式不同。绑定时它是一个右值引用,但之后它就像一个左值引用。在所有方面,右值引用类型的变量在绑定完成后就像左值引用类型的变量一样。

左值和右值引用之间的唯一区别在于绑定时 - 如果同时存在左值和右值重载可用,则临时对象(或者更确切地说是 xvalues - eXpiring 值)将优先绑定到右值引用:

void goo(const int& x) {}
void goo(int&& x) {}

goo(5); // this will call goo(int&&) because 5 is an xvalue

这是唯一的区别。从技术上讲,除了约定之外,没有什么能阻止您使用像左值引用这样的右值引用:

void doit(int&& x) {
    x = 123;
}

int a;
doit(std::move(a));
std::cout << a; // totally valid, prints 123, but please don't do that

这里的关键词是“约定”。由于右值引用优先绑定到临时对象,因此可以合理地假设您可以删除临时对象,即将其所有数据移开,因为在调用之后它无法以任何方式访问并且无论如何都会被销毁:

std::vector<std::string> strings;
string.push_back(std::string("abc"));

在上面的代码片段中,临时对象std::string("abc")不能在它出现的语句之后以任何方式使用,因为它没有绑定到任何变量。因此push_back允许移走其内容而不是复制它,从而节省额外的分配和释放。

也就是说,除非您使用std::move

std::vector<std::string> strings;
std::string mystr("abc");
string.push_back(std::move(mystr));

现在该对象mystr在调用 之后仍然可以访问push_back,但push_back不知道这一点 - 它仍然假设它被允许删除该对象,因为它是作为右值引用传入的。这就是为什么 的行为std::move()是一种惯例,也是为什么std::move()它本身实际上并没有做任何事情 - 特别是它没有做任何动作。它只是将其论点标记为“准备好被淘汰”。


最后一点是:右值引用仅在与左值引用一起使用时才有用。没有右值参数本身有用的情况(这里夸大了)。

假设您有一个接受字符串的函数:

void foo(std::string);

如果函数只是检查字符串而不是复制它,那么使用const&

void foo(const std::string&);

这总是在调用函数时避免复制。

如果函数要修改或存储字符串的副本,则使用按值传递:

void foo(std::string s);

在这种情况下,如果调用者传递一个左值,您将收到一份副本,并且临时对象将在原地构造,避免复制。std::move(s)如果您想存储 s 的值,则使用,例如在成员变量中。请注意,即使调用者传递了一个右值引用,这也会有效地工作,foo(std::move(mystring));因为std::string它提供了一个移动构造函数。

在这里使用右值是一个糟糕的选择:

void foo(std::string&&)

因为它将准备对象的负担放在调用者身上。特别是如果调用者想要将一个字符串的副本传递给这个函数,他们必须明确地这样做;

std::string s;
foo(s); // XXX: doesn't compile
foo(std::string(s)); // have to create copy manually

如果您想将可变引用传递给变量,只需使用常规左值引用:

void foo(std::string&);

在这种情况下使用右值引用在技术上是可行的,但在语义上不正确且完全令人困惑。

右值引用唯一有意义的地方是移动构造函数或移动赋值运算符。在任何其他情况下,按值传递或左值引用通常是正确的选择,可以避免很多混乱。


注意:不要将右值引用与看起来完全相同但工作方式完全不同的转发引用混淆,如下所示:

template <class T>
void foo(T&& t) {
}

在上面的例子中t看起来像一个右值引用参数,但实际上是一个转发引用(因为模板类型),这是一个完全不同的蠕虫罐。

于 2020-03-04T11:01:08.550 回答