【问题标题】:Understanding lvalue/rvalue expression vs object type了解左值/右值表达式与对象类型
【发布时间】:2019-01-23 02:00:17
【问题描述】:

我已经阅读了一些之前的热门答案以及 Stroustrup 的“The C++ Programming Language”和“Effective Modern C++”,但我很难真正理解表达式的左值/右值方面与其类型。在“Effective Modern C++”的介绍中说:

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

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

编辑:

要跟进下面的一些答案/cmets,仍然不清楚的是为什么在下面的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());
}

【问题讨论】:

  • 这里似乎存在一些根本性的混淆; “引用”属于类型,左值/右值属于表达式。表达式永远不是引用,引用也永远不是表达式。左值引用绑定左值,右值引用绑定右值,这就是他们名字的意思。
  • @KerrekSB 其实没那么简单;以const T& 为例。
  • @LightnessRacesinOrbit: ...叹息,是的,当然,但首先近似。我想指出高层次的概念错误,而不用过多的细节混淆图片。

标签: c++ c++11 move move-semantics


【解决方案1】:

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

正如引用所说,typesvalue categories 是不同的东西。参数始终是左值,即使它的类型是右值引用;我们必须使用std::move 将其绑定到右值引用。假设我们允许编译器隐式地做,像下面的代码sn-p,

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
}

【讨论】:

    【解决方案2】:

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

    只有从正在解决的问题的角度来看,这才有意义。这是一个很好的link,专门用于解释 RValue 引用。

    【讨论】:

      【解决方案3】:

      我认为您实际上已经掌握了 typevalue category 之间的区别,所以我将重点关注两个具体的声明/查询:

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

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

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-07-07
        • 2015-04-14
        相关资源
        最近更新 更多