了解左值/右值表达式与对象类型

Understanding lvalue/rvalue expression vs object type

本文关键字:对象 表达式 类型 了解      更新时间:2023-10-16

我已经阅读了一些先前的热门答案以及Stroustrup的"C++编程语言"和"有效的现代C++",但我很难真正理解表达式的左值/右值方面与其类型之间的区别。在"有效的现代C++"的引言中,它说:

确定表达式是否为左值的一个有用的启发式方法是询问是否可以获取其地址。如果可以的话,通常是这样。如果你不能,它通常是一个右值。这种启发式的一个很好的功能是,它可以帮助您记住表达式的类型与表达式是左值还是右值无关......在处理右值引用类型的参数时,记住这一点尤其重要,因为参数本身是一个左值。

我不明白什么,因为我不明白为什么如果你有一个右值引用类型参数,你需要通过std::move()实际将其转换为右值以使其有资格被移动。即使参数(所有参数)是左值,编译器也知道其类型是右值引用,那么为什么需要告诉编译器它可以移动呢?这似乎是多余的,但我想我不明白表达式类型与其左值/右值性质之间的区别(不确定正确的术语)。

编辑:

为了跟进下面的一些答案/评论,仍然不清楚的是为什么在下面的doSomething()中我需要将参数包装在std::move()中,以使其绑定到右值引用并解析为doSomethingElse()的第二个版本。我知道,如果这隐式发生,那将是糟糕的,因为参数将被移出,并且在此之后可能会无意中使用它。似乎参数的右值引用类型性质在函数中毫无意义,因为它的唯一目的是绑定以解析到函数的正确版本,给定右值作为参数传入。

Widget getWidget();
void doSomethingElse(Widget& rhs);  // #1
void doSomethingElse(Widget&& rhs); // #2
void doSomething(Widget&& rhs) {
// will call #1
doSomethingElse(rhs);
// will call #2
doSomethingElse(std::move(rhs));      
}
int main() {
doSomething(getWidget());
}
我不

明白为什么如果你有一个右值引用类型参数,你需要通过std::move()实际将其转换为右值以使其有资格被移动。

正如引号所说,类型和值类别是不同的东西。参数始终是左值,即使其类型是右值引用;我们必须使用std::move将其绑定到右值引用。假设我们允许编译器隐式执行此操作,如以下代码片段,

void foo(std::string&& s);
void bar(std::string&& s) {
foo(s);  
// continue to use s...
// oops, s might have been moved
foo(std::string{}); // this is fine;
// the temporary will be destroyed after the full expression and won't be used later
}

因此,我们必须明确地使用std::move,告诉编译器我们知道我们要做什么。

void bar(std::string&& s) {
foo(std::move(s));  
// we know that s might have been moved
}

存在RValue 引用来解决转发问题。C++中现有的类型推导规则使得不可能具有一致且合理的移动语义。因此,类型系统得到了扩展,并引入了新的规则,使其更加复杂但一致。

只有当你从正在解决的问题的角度来看待它时,这才有意义。这里有一个很好的链接,专门用于解释 RValue 参考。

我认为您实际上已经掌握了类型和值类别之间的区别,因此我将重点介绍两个特定的声明/查询:

您需要通过 std::move() 实际将其转换为右值,以使其有资格被移动

有点,但不是真的。将命名或引用对象的表达式强制转换为右值允许我们在重载解析期间触发函数重载Type&&.按照惯例,当我们想要转让所有权时,我们会这样做,但这并不完全等同于使其"有资格被移动",因为移动可能不是您最终要做的事情。从某种意义上说,这是一种吹毛求疵,尽管我认为理解这一点很重要。因为:

即使参数(所有参数)是左值,编译器也知道其类型是右值引用,那么为什么需要告诉编译器它可以移动呢?

除非你写std::move(theThing),或者这个东西是一个临时的(已经是一个右值),那么它就不是右值,所以它不能绑定到右值引用。这就是一切的设计和定义方式。它是故意这样做的,以便左值表达式(命名事物的表达式)或您没有std::move()编写的事物不会绑定到右值引用。因此,要么你的程序不会编译,要么,如果可用,重载解析将选择一个可能需要const Type&的函数版本——我们知道这不会涉及所有权转让。

tl;DR:编译器不知道它的类型是右值引用,因为它不是。就像你不能做int& ref = 42一样,你不能做int x = 42; int&& ref = x;。否则,它会尝试移动所有东西!重点是使某些类型的引用仅适用于某些类型的表达式,以便我们可以使用它来触发对复制/移动函数的调用,以在调用站点使用最少的机器。