在函数内部使用时,rvalue引用是否被视为lvalue?

46
我发布了这个答案:https://dev59.com/OIfca4cB1Zd3GeqPnMXi#28459180,其中包含以下代码:
void foo(string&& bar){
    string* temp = &bar;

    cout << *temp << " @:" << temp << endl;
}

“bar” 是一个 rvalue 还是一个 lvalue?

我问这个问题是因为显然我不能获取一个 rvalue 的地址,但是我可以获取一个 rvalue 引用的地址,就像这里所做的那样。

如果你可以对一个 rvalue 引用执行与 lvalue 引用相同的操作,那么使用 "&&" 而不是 "&" 来区分两者有什么意义呢?


2
rvalue 的整个意义在于允许移动语义,它们在大多数方面(除了它们的类型)与引用相同。 - Guvante
2
在你的例子中,你并没有移动两次,而是复制了两次,因为 bar 是一个左值(它的类型是右值,很奇怪)。然而,如果你调用了 std::move 两次,可能会遇到问题。 - Guvante
3
这里对bar进行了两次使用,正是因为这样的双重用途才使得bar是一个左值(lvalue)——这样双重使用就不会导致问题。只有在明确使用std::move(bar)时,它才会被视为右值(rvalue)。 - Angew is no longer proud of SO
1
@JonathanMee:你的思路有些偏颇。右值引用是被移动而不是移动出去。当调用你的函数时,你知道bar将会被销毁,这就是你得到的结果。但是,如果你在你的函数内部调用另一个函数,除非你明确表示,否则你不能保证这一点。 - Guvante
5
“我们为什么要称它为右值引用?”- 因为它是对右值的引用(或者更严谨地说,对由右值表达式表示的对象的引用)。这意味着它指向一个临时变量,或者一个被显式移动的变量(使用 std::move 或等效方法),因此您的函数可以假设可以从中移动数据。 - Mike Seymour
显示剩余4条评论
4个回答

71

bar是rvalue还是lvalue?

问题自问自答。任何有名字的东西都是lvalue(1)。所以bar是一个lvalue。它的类型是“rvalue reference to string”,但它是该类型的lvalue。

如果您想将其视为rvalue,则需要对其应用std::move()


如果您可以在rvalue引用上执行任何操作,就像在lvalue引用上执行一样,那么使用“&&”而不仅仅是“&”有什么意义?

这取决于您对“执行操作”的定义。在表达式中,lvalue引用和(命名的)rvalue引用几乎相同,但它们在绑定到它们的内容方面有很大差别。lvalue可以绑定到lvalue引用,rvalue可以绑定到rvalue引用(任何东西都可以绑定到const lvalue引用)。也就是说,您不能将rvalue绑定到lvalue引用,反之亦然。

让我们来谈谈rvalue引用类型的函数参数(例如您的bar)。重要的不是bar是什么,而是您了解bar所指向的值。由于bar是一个rvalue引用,您确定绑定到它的任何内容都是rvalue。这意味着在整个表达式结束时,该值将被销毁,并且您可以安全地将其视为rvalue(通过窃取其资源等)。

如果您并非直接使用bar,而只是想传递bar,则有两个选项:要么您完成了对bar的使用,那么您应该告诉下一个接收者它绑定了一个右值-—执行std::move(bar)。要么您需要对bar进行更多操作,因此不希望中间有任何人从下面夺走它的资源,所以将其视为左值-—bar

总结一下:区别不在您获得引用后可以做什么。区别在于哪些内容可以与引用绑定。


(1)一个好的经验法则,有少量例外:枚举器有名称,但是是右值。类、命名空间和类模板具有名称,但不是值。


1
@JonathanMee 这是一个指向可以安全地视为rvalue的东西的引用(也就是说,您可以在其上调用std::move,而不会冒着使其他地方可能期望保持有效的对象无效的风险)。 - jalf
3
不,这不仅仅是对程序员的提示。这是一个硬性规定,适用于外部。区别不在于一旦拥有引用,你可以做什么。而在于什么可以绑定到该引用上。 - Angew is no longer proud of SO
1
任何拥有名称的东西都是lvalue。严格来说,枚举器是rvalue。是否有一个简单的规则可以协调这一点?(例如,“变量和函数的名称是lvalue”) - dyp
@dyp 总是有令人烦恼的细节,对吧?已经修改。 - Angew is no longer proud of SO
2
@dyp:官方定义非常简单:“lvalue表示函数或对象”。名称无关紧要:并非所有函数和对象都有名称,也并非所有有名称的东西都是lvalue - Mike Seymour
显示剩余9条评论

13

bar是rvalue还是lvalue?

它是一个lvalue,与任何命名变量的表达式一样。

如果您可以对rvalue引用执行与lvalue引用相同的任何操作,则使用“&&”而不仅仅是“&”有什么意义?

您只能使用rvalue表达式初始化rvalue引用。因此,您可以将临时字符串传递给函数,但不能传递字符串变量,除非明确从中移动。这意味着您的函数可以假定参数已不再需要,并且可以从中移动。

std::string variable;
foo(variable);            // ERROR, can't pass an lvalue
foo(std::move(variable)); // OK, can explicitly move
foo("Hello");             // OK, can move from a temporary

那么你的意思是,rvalue引用的目的是告诉编译器,在使用该值构造函数时可以使用移动构造函数?如果我初始化两次,这不会导致问题吗?比如 string temp1(bar), temp2(bar); - Jonathan Mee
3
不,rvalue引用的目的是只能绑定到rvalue。在该示例中,bar是一个lvalue,因此两个temp变量都使用复制构造函数(需要一个lvalue引用)进行初始化,而不是移动构造函数(需要一个rvalue引用),这可以避免如果第一个构造函数从bar中移动而导致的任何问题。 - Mike Seymour

0

bar表达式是一个左值。但它不是一个“字符串的右值引用”。bar表达式是一个左值引用。您可以通过在foo()中添加以下行来验证:

cout << is_lvalue_reference<decltype((bar))>::value << endl; // prints "1" 

但我同意Angew的解释中的其余部分。

一个右值引用,在绑定到一个右值之后,就成为了一个左值引用。实际上,它不仅仅适用于函数参数:

string&& a = "some string";
string&& b = a; // ERROR: it does not compile because a is not an rvalue

如果您可以在右值引用上执行与左值引用相同的任何操作,那么使用“&&”而不是“&”有什么意义呢?
右值引用允许我们在右值过期之前执行一些“左值操作”。

需要澄清的是:目前说“bar的类型”是不明确的。标记bar在语法上可以是一个表达式,也可以不是一个表达式。如果它在语法上是一个表达式,那么它的类型就是std::string(不是引用)。然而,如果它在语法上是decltype(id-expression)的操作数,则其类型为std::string&& - M.M

-2
在这种情况下,命名的右值引用是左值,而如果输入类型为const 命名的右值引用,那么它将是右值,以下是一个简单的例子:
#include <string>
#include <iostream>

void foo(const std::string&& bar) { 
    std::string* temp = &bar; // compile error, can't get address
    std::cout << *temp << " @:" << temp << std::endl;
}

int main()
{
    foo("test");
    return 0;
}

希望这对你有用。

这似乎是真的。你能提供一个来源吗? - Jonathan Mee
希望这个链接有用:https://codesynthesis.com/~boris/blog//2012/07/24/const-rvalue-references/ - Kehe CAI
2
这是错误的。在这里,bar 仍然是一个左值;问题在于 &bar 的类型为 const std::string *,因此无法分配给 std::string*,这将丢弃 const 限定符。如果您将代码更改为 const std::string* temp =,那么它将编译得很好。 - M.M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接