1547

在 C++03 中,表达式是rvaluelvalue

在 C++11 中,表达式可以是:

  1. 右值
  2. 左值
  3. 极值
  4. 左值
  5. 公允价值

两个类别变成了五个类别。

  • 这些新的表达方式是什么?
  • 这些新类别与现有的右值和左值类别有何关系?
  • C++0x 中的右值和左值类别是否与 C++03 中的相同?
  • 为什么需要这些新类别?WG21 的众神只是想迷惑我们这些凡人吗
4

13 回答 13

705

我想这个文件可能作为一个不那么简短的介绍:n3055

整个屠杀从移动语义开始。一旦我们有了可以移动而不是复制的表达式,突然容易掌握的规则要求区分可以移动的表达式以及移动的方向。

从我基于草案的猜测来看,r/l 值的区别保持不变,只是在移动事物变得混乱的情况下。

他们需要吗?如果我们希望放弃新功能,可能不会。但是为了实现更好的优化,我们可能应该接受它们。

引用n3055

  • 左值(所谓的,历史上,因为左值可能出现在赋值表达式的左侧)指定一个函数或一个对象。 [例子:ifE是指针类型的表达式,then*E 是一个左值表达式,指的是指向的对象或函数E 。再举一个例子,调用返回类型为左值引用的函数的结果是左值。]
  • 一个xvalue(一个“eXpiring”值)也指一个对象,通常接近其生命周期的末尾(例如,它的资源可能会被移动)。xvalue 是某些涉及右值引用的表达式的结果。 [示例:调用返回类型为右值引用的函数的结果是 xvalue。]
  • 值 (“广义”左值)是左值xvalue
  • 右值(所谓的,历史上,因为右值可能出现在赋值表达式的右侧)是一个 xvalue,一个临时对象或其子对象,或者一个不与对象关联的值。
  • prvalue(“纯”右值)是不是 xvalue 的右值 。 [示例:调用返回类型不是引用的函数的结果是prvalue]

有问题的文件是这个问题的一个很好的参考,因为它显示了由于引入新命名法而发生的标准的确切变化。

于 2010-08-30T15:09:16.103 回答
363

这些新的表达方式是什么?

FCD(n3092)有一个很好的描述:

— 左值(历史上称为左值,因为左值可能出现在赋值表达式的左侧)指定函数或对象。[ 示例:如果 E 是指针类型的表达式,则 *E 是一个左值表达式,指的是 E 指向的对象或函数。作为另一个示例,调用返回类型为左值引用的函数的结果是左值。—结束示例]

— 一个 xvalue(一个“eXpiring”值)也指一个对象,通常接近其生命周期的末尾(例如,它的资源可能会被移动)。xvalue 是某些涉及右值引用的表达式的结果 (8.3.2)。[ 示例:调用返回类型为右值引用的函数的结果是 xvalue。—结束示例]

— 左值(“广义”左值)是左值或 xvalue。

— 右值(历史上称为右值,因为右值可能出现在赋值表达式的右侧)是 xvalue、临时对象 (12.2) 或其子对象,或与对象无关的值。

— prvalue(“纯”右值)是不是 xvalue 的右值。[ 示例:调用返回类型不是引用的函数的结果是纯右值。诸如 12、7.3e5 或 true 之类的文字的值也是纯右值。—结束示例]

每个表达式都属于此分类法中的基本分类之一:左值、xvalue 或 prvalue。表达式的这个属性称为它的值类别。[ 注意:第 5 章中对每个内置运算符的讨论表明了它产生的值的类别以及它所期望的操作数的值类别。例如,内置赋值运算符期望左操作数是左值,右操作数是纯右值并产生左值作为结果。用户定义的运算符是函数,它们期望和产生的值的类别由它们的参数和返回类型决定。——尾注

不过,我建议您阅读整个第3.10 节左值和右值

这些新类别与现有的右值和左值类别有何关系?

再次:

分类

C++0x 中的右值和左值类别是否与 C++03 中的相同?

右值的语义随着移动语义的引入而发展。

为什么需要这些新类别?

这样就可以定义和支持移动构造/分配。

于 2010-08-30T15:18:36.363 回答
203

我将从你的最后一个问题开始:

为什么需要这些新类别?

C++ 标准包含许多处理表达式的值类别的规则。一些规则区分左值和右值。例如,当涉及到重载决议时。其他规则对 glvalue 和 prvalue 进行了区分。例如,您可以有一个不完整或抽象类型的泛左值,但没有不完整或抽象类型的纯右值。在我们使用这个术语之前,实际上需要区分 glvalue/prvalue 的规则指的是 lvalue/rvalue,它们要么是无意错误的,要么包含许多对规则 a la “的解释和例外”......除非 rvalue 是由于未命名的右值引用..."。所以,给glvalues和prvalues的概念自己命名似乎是个好主意。

这些新的表达方式是什么?这些新类别与现有的右值和左值类别有何关系?

我们仍然有与 C++98 兼容的术语左值和右值。我们只是将右值分为两个子组,xvalues 和 prvalues,我们将 lvalues 和 xvalues 称为 glvalues。Xvalues 是一种用于未命名右值引用的新型值类别。每个表达式都是以下三个之一:左值、xvalue、prvalue。维恩图如下所示:

    ______ ______
   /      X      \
  /      / \      \
 |   l  | x |  pr  |
  \      \ /      /
   \______X______/
       gl    r

函数示例:

int   prvalue();
int&  lvalue();
int&& xvalue();

但也不要忘记命名的右值引用是左值:

void foo(int&& t) {
  // t is initialized with an rvalue expression
  // but is actually an lvalue expression itself
}
于 2010-08-31T08:08:28.323 回答
190

为什么需要这些新类别?WG21 的众神只是想迷惑我们这些凡人吗?

我不认为其他答案(虽然很多都很好)真正抓住了这个特定问题的答案。是的,存在这些类别等以允许移动语义,但存在复杂性是有原因的。这是在 C++11 中移动内容的一个不可违反的规则:

只有在毫无疑问是安全的情况下才能移动。

这就是存在这些类别的原因:能够在安全的地方谈论价值观,并在不安全的地方谈论价值观。

在最早版本的 r 值引用中,移动很容易发生。容易了。很容易,当用户并不真正想要时,有很多潜在的隐式移动东西。

以下是可以安全移动某物的情况:

  1. 当它是临时对象或子对象时。(prvalue)
  2. 当用户明确表示要移动它时

如果你这样做:

SomeType &&Func() { ... }

SomeType &&val = Func();
SomeType otherVal{val};

这是做什么的?在旧版本的规范中,在 5 个值出现之前,这会引起移动。当然可以。您将右值引用传递给构造函数,因此它绑定到采用右值引用的构造函数。这很明显。

这只有一个问题。你没有要求移动它。哦,你可能会说这&&应该是一个线索,但这并不能改变它违反规则的事实。val不是临时的,因为临时的没有名字。您可能已经延长了临时的生命周期,但这意味着它不是临时的;它就像任何其他堆栈变量一样。

如果它不是临时的,并且你没有要求移动它,那么移动是错误的。

显而易见的解决方案是创建val一个左值。这意味着你不能离开它。好的; 它被命名,所以它是一个左值。

一旦你这样做了,你就不能再说这在任何地方都SomeType&&意味着同样的事情。您现在已经区分了命名右值引用和未命名右值引用。好吧,命名的右值引用是左值;这就是我们上面的解决方案。那么我们称之为未命名的右值引用(Func上面的返回值)是什么?

它不是左值,因为你不能从左值移动。我们需要能够通过返回一个&&;来移动。你怎么能明确地说要移动一些东西?std::move毕竟,这就是回报。它不是右值(旧式),因为它可以在等式的左侧(事情实际上有点复杂,请参阅这个问题和下面的评论)。它既不是左值也不是右值;这是一种新事物。

我们拥有的是一个可以视为左值的值,除了它可以隐式移动。我们称之为 xvalue。

请注意,xvalues 是使我们获得其他两类值的原因:

  • prvalue 实际上只是前一种右值类型的新名称,即它们是不是xvalue 的右值。

  • Glvalues 是一组中的 xvalues 和 lvalues 的联合,因为它们确实共享许多共同的属性。

所以真的,这一切都归结为 xvalues 以及将移动限制在特定位置的需要。这些地方由右值类别定义;prvalues 是隐式移动,xvalues 是显式移动(std::move返回一个 xvalue)。

于 2012-03-04T06:32:42.690 回答
159

恕我直言,关于其含义的最佳解释给了我们Stroustrup + 考虑Dániel SándorMohan的例子:

斯特鲁普:

现在我非常担心。显然,我们正走向僵局或一团糟,或两者兼而有之。我花了午餐时间进行分析,看看哪些属性(值)是独立的。只有两个独立的属性:

  • has identity– 即和地址,一个指针,用户可以判断两个副本是否相同等。
  • can be moved from– 即我们被允许以某种不确定但有效的状态离开“副本”的来源

这使我得出结论,确切地说存在三种值(使用大写字母表示否定的正则表达式符号技巧——我很着急):

  • iM: 有身份,不能搬离
  • im: 具有标识并且可以从其中移动(例如,将左值转换为右值引用的结果)
  • Im: 没有身份,可以搬离。

    第四种可能性 , IM(没有身份并且无法移动)在C++(或者,我认为)任何其他语言中都没有用。

除了这三个基本的值分类之外,我们还有两个明显的概括对应于两个独立的属性:

  • i: 有身份
  • m: 可以从

这导致我把这张图放在板上: 在此处输入图像描述

命名

我观察到我们只有有限的命名自由:左边的两点(标记iMi)是或多或少正式的人所称lvalues的,右边的两点(标记 mIm)是或多或少正式的人打过电话rvalues。这必须反映在我们的命名中。也就是说,左“腿”W应该有相关的名称lvalue,右“腿”W应该有相关的名称rvalue.我注意到这整个讨论/问题源于右值引用和移动语义的引入。rvalues这些概念在 Strachey 的由和组成的世界中根本不存在lvalues。有人观察到

  • 每个value都是一个lvalue或一个rvalue
  • 一个lvalue不是一个rvalue并且一个rvalue不是一个lvalue

深深植根于我们的意识中,非常有用的属性,并且在整个标准草案中都可以找到这种二分法的痕迹。我们都同意我们应该保留这些属性(并使它们精确)。这进一步限制了我们的命名选择。我观察到标准库的措辞用来rvalue表示(泛化),因此为了保留标准库的期望和文本,应该命名 m的右下点Wrvalue.

这导致了对命名的集中讨论。首先,我们需要决定lvalue.应该lvalue意味着iM还是泛化i?在 Doug Gregor 的带领下,我们列出了核心语言措辞中该词lvalue有资格表示其中一个或另一个的位置。列出了一个列表,在大多数情况下,目前最棘手/最脆弱的文本 lvalue意味着iM. 这是左值的经典含义,因为“在过去”没有任何东西被移动;move是 中的一个新概念C++0x。此外,命名 的左上角W lvalue给我们一个属性,即每个值都是 anlvalue或 an rvalue,但不是两者兼而有之。

那么, 的左上点Wlvalue右下点是rvalue.什么是左下点和右上点?左下点是经典左值的泛化,允许移动。所以它是一个generalized lvalue.我们命名它 glvalue.你可以对缩写提出质疑,但(我认为)不符合逻辑。我们假设在认真使用时generalized lvalue 无论如何都会以某种方式缩写,所以我们最好立即这样做(或冒着混淆的风险)。W 的右上角没有右下角那么通用(现在和以往一样,称为rvalue)。该点代表了您可以从中移动的对象的原始纯概念,因为它不能被再次引用(除了析构函数)。我喜欢这个短语specialized rvaluegeneralized lvaluepure rvalue缩写为prvalue赢了(可能是正确的)。所以,W的左腿是lvalueand glvalue右腿是prvalueandrvalue.顺便提一下,每个值要么是左值,要么是纯右值,但不是两者兼而有之。

这留下了W:im的顶部中间 也就是说,具有标识且可以移动的值。我们真的没有任何东西可以引导我们为那些深奥的野兽起个好名字。它们对使用(草稿)标准文本的人很重要,但不太可能成为家喻户晓的名字。我们没有发现任何真正的命名限制来指导我们,所以我们选择了“x”作为中心、未知、奇怪、仅限专家,甚至是 x 级。

史蒂夫展示最终产品

于 2016-07-03T12:30:23.583 回答
81

介绍

ISOC++11(正式名称为 ISO/IEC 14882:2011)是 C++ 编程语言标准的最新版本。它包含一些新功能和概念,例如:

  • 右值引用
  • xvalue、glvalue、prvalue 表达式值类别
  • 移动语义

如果我们想了解新表达式值类别的概念,我们必须知道存在右值和左值引用。最好知道右值可以传递给非常量右值引用。

int& r_i=7; // compile error
int&& rr_i=7; // OK

如果我们引用工作草案 N3337(与已发布的 ISOC++11 标准最相似的草案)中标题为 Lvalues 和 rvalues 的小节,我们可以对值类别的概念有一些直觉。

3.10 左值和右值 [basic.lval]

1 表达式根据图 1 中的分类法进行分类。

  • 左值(历史上称为左值,因为左值可能出现在赋值表达式的左侧)指定函数或对象。[ 示例:如果 E 是指针类型的表达式,则 *E 是一个左值表达式,指的是 E 指向的对象或函数。作为另一个示例,调用返回类型为左值引用的函数的结果是左值。—结束示例]
  • 一个 xvalue(一个“eXpiring”值)也指一个对象,通常接近其生命周期的末尾(例如,它的资源可能会被移动)。xvalue 是某些涉及右值引用的表达式的结果 (8.3.2)。[ 示例:调用返回类型为右值引用的函数的结果是 xvalue。—结束示例]
  • 左值(“广义”左值)是左值或 xvalue。
  • 右值(历史上称为右值,因为右值可能出现在赋值表达式的右侧)是一个 xvalue、一个
    临时对象 (12.2) 或其子对象,或者一个
    与对象无关的值。
  • prvalue(“纯”右值)是不是 xvalue 的右值。[ 示例:调用返回类型不是
    引用的函数的结果是纯右值。诸如 12、7.3e5 或
    true 之类的文字的值也是纯右值。—结束示例]

每个表达式都属于此分类法中的基本分类之一:左值、xvalue 或 prvalue。表达式的这个属性称为它的值类别。

但我不太确定这一小节是否足以清楚地理解这些概念,因为“通常”不是很笼统,“接近生命周期的尽头”不是很具体,“涉及右值引用”不是很清楚,和“示例:调用返回类型为右值引用的函数的结果是 xvalue。” 听起来像是一条蛇在咬它的尾巴。

主要价值类别

每个表达式都恰好属于一个主要值类别。这些值类别是左值、xvalue 和 prvalue 类别。

左值

表达式 E 属于左值类别当且仅当 E 引用一个实体,该实体已经具有使其在 E 之外可访问的身份(地址、名称或别名)。

#include <iostream>

int i=7;

const int& f(){
    return i;
}

int main()
{
    std::cout<<&"www"<<std::endl; // The expression "www" in this row is an lvalue expression, because string literals are arrays and every array has an address.  

    i; // The expression i in this row is an lvalue expression, because it refers to the same entity ...
    i; // ... as the entity the expression i in this row refers to.

    int* p_i=new int(7);
    *p_i; // The expression *p_i in this row is an lvalue expression, because it refers to the same entity ...
    *p_i; // ... as the entity the expression *p_i in this row refers to.

    const int& r_I=7;
    r_I; // The expression r_I in this row is an lvalue expression, because it refers to the same entity ...
    r_I; // ... as the entity the expression r_I in this row refers to.

    f(); // The expression f() in this row is an lvalue expression, because it refers to the same entity ...
    i; // ... as the entity the expression f() in this row refers to.

    return 0;
}

极值

表达式 E 属于 xvalue 范畴当且仅当它是

— 调用函数的结果,无论是隐式还是显式,其返回类型是对所返回对象类型的右值引用,或者

int&& f(){
    return 3;
}

int main()
{
    f(); // The expression f() belongs to the xvalue category, because f() return type is an rvalue reference to object type.

    return 0;
}

— 转换为对对象类型的右值引用,或

int main()
{
    static_cast<int&&>(7); // The expression static_cast<int&&>(7) belongs to the xvalue category, because it is a cast to an rvalue reference to object type.
    std::move(7); // std::move(7) is equivalent to static_cast<int&&>(7).

    return 0;
}

— 一个类成员访问表达式,指定一个非引用类型的非静态数据成员,其中对象表达式是一个 xvalue,或

struct As
{
    int i;
};

As&& f(){
    return As();
}

int main()
{
    f().i; // The expression f().i belongs to the xvalue category, because As::i is a non-static data member of non-reference type, and the subexpression f() belongs to the xvlaue category.

    return 0;
}

— 指向成员的表达式,其中第一个操作数是 xvalue,第二个操作数是指向数据成员的指针。

请注意,上述规则的效果是,对对象的命名右值引用被视为左值,对对象的未命名右值引用被视为 xvalue;对函数的右值引用被视为左值,无论是否命名。

#include <functional>

struct As
{
    int i;
};

As&& f(){
    return As();
}

int main()
{
    f(); // The expression f() belongs to the xvalue category, because it refers to an unnamed rvalue reference to object.
    As&& rr_a=As();
    rr_a; // The expression rr_a belongs to the lvalue category, because it refers to a named rvalue reference to object.
    std::ref(f); // The expression std::ref(f) belongs to the lvalue category, because it refers to an rvalue reference to function.

    return 0;
}

prvalues

当且仅当 E 既不属于左值也不属于 xvalue 范畴时,表达式 E 属于纯右值范畴。

struct As
{
    void f(){
        this; // The expression this is a prvalue expression. Note, that the expression this is not a variable.
    }
};

As f(){
    return As();
}

int main()
{
    f(); // The expression f() belongs to the prvalue category, because it belongs neither to the lvalue nor to the xvalue category.

    return 0;
}

混合价值类别

还有两个更重要的混合价值类别。这些值类别是右值和左值类别。

右值

当且仅当 E 属于 xvalue 类别或 prvalue 类别时,表达式 E 属于 rvalue 类别。

请注意,此定义意味着表达式 E 属于右值类别当且仅当 E 引用的实体没有任何身份使其在 E YET 之外可访问。

左值

当且仅当 E 属于左值类别或 xvalue 类别时,表达式 E 属于左值类别。

实用规则

Scott Meyer发布了一个非常有用的经验法则来区分右值和左值。

  • 如果您可以获取表达式的地址,则该表达式是左值。
  • 如果表达式的类型是左值引用(例如,T& 或 const T& 等),则该表达式是左值。
  • 否则,表达式是一个右值。从概念上(通常实际上也是),右值对应于临时对象,例如从函数返回或通过隐式类型转换创建的对象。大多数文字值(例如,10 和 5.3)也是右值。
于 2016-01-20T13:46:20.917 回答
44

我已经为此苦苦挣扎了很长时间,直到我遇到了 cppreference.com 对value categories的解释。

它实际上相当简单,但我发现它经常以难以记忆的方式解释。这里非常示意性地解释了它。我将引用页面的某些部分:

主要类别

主要值类别对应于表达式的两个属性:

  • 具有身份:可以确定表达式是否与另一个表达式引用相同的实体,例如通过比较对象的地址或它们标识的函数(直接或间接获得);

  • 可以从:移动构造函数、移动赋值运算符或实现移动语义的另一个函数重载可以绑定到表达式。

表示:

  • 具有标识且不能被移动的称为左值表达式
  • 具有身份并且可以从中移动的称为xvalue 表达式
  • 没有身份并且可以移动的称为prvalue表达式
  • 没有身份,不能从不使用。

左值

左值(“左值”)表达式是具有标识不能从 中移动的表达式。

右值(C++11 前)、右值(C++11 起)

prvalue(“纯右值”)表达式是一个没有标识的表达式,可以从.

极值

一个 xvalue(“过期值”)表达式是一个具有标识并且可以从.

左值

左值(“广义左值”)表达式是左值或 xvalue 的表达式。它有身份。它可能会或可能不会被移动。

右值 (C++11 起)

右值(“右值”)表达式是纯右值或 xvalue 的表达式。它可以从. 它可能有也可能没有身份。

所以让我们把它放到一个表中:

可以从 (= rvalue) 移动 无法从
有身份(= glvalue) 极值 左值
没有身份 公允价值 不曾用过
于 2016-06-17T02:20:32.997 回答
36

C++03 的类别过于受限,无法正确地将右值引用引入到表达式属性中。

随着它们的引入,据说一个未命名的右值引用评估为一个右值,这样重载决议会更喜欢右值引用绑定,这将使它选择移动构造函数而不是复制构造函数。但发现这会导致各种问题,例如动态类型和资格。

为了证明这一点,考虑

int const&& f();

int main() {
  int &&i = f(); // disgusting!
}

在 xvalue 之前的草稿中,这是允许的,因为在 C++03 中,非类类型的右值永远不是 cv 限定的。但它const适用于右值引用的情况,因为这里我们确实引用了对象(= 内存!),并且从非类右值中删除 const 主要是因为周围没有对象。

动态类型的问题具有相似的性质。在 C++03 中,类类型的右值具有已知的动态类型——它是该表达式的静态类型。因为要以另一种方式使用它,您需要引用或取消引用,它们评估为左值。未命名的右值引用并非如此,但它们可以显示多态行为。所以要解决它,

  • 未命名的右值引用成为xvalues。它们可以是合格的,并且可能具有不同的动态类型。它们确实像预期的那样在重载期间更喜欢右值引用,并且不会绑定到非常量左值引用。

  • 以前是右值(文字,由强制转换为非引用类型创建的对象)现在变成了纯右值。它们在重载期间具有与 xvalues 相同的偏好。

  • 以前是左值的东西仍然是左值。

并且完成了两个分组来捕获那些可以限定并且可以具有不同动态类型(glvalues)的分组以及那些重载更喜欢右值引用绑定(rvalues)的分组。

于 2010-08-30T16:46:28.920 回答
24

由于前面的答案详尽地涵盖了价值类别背后的理论,我还想补充一件事:您可以实际使用它并对其进行测试。

对于值类别的一些动手实验,您可以使用decltype 说明符。它的行为明确区分了三个主要值类别(xvalue、lvalue 和 prvalue)。

使用预处理器为我们节省了一些打字...

主要类别:

#define IS_XVALUE(X) std::is_rvalue_reference<decltype((X))>::value
#define IS_LVALUE(X) std::is_lvalue_reference<decltype((X))>::value
#define IS_PRVALUE(X) !std::is_reference<decltype((X))>::value

混合类别:

#define IS_GLVALUE(X) (IS_LVALUE(X) || IS_XVALUE(X))
#define IS_RVALUE(X) (IS_PRVALUE(X) || IS_XVALUE(X))

现在我们可以(几乎)重现cppreference 中关于 value category的所有示例。

以下是 C++17 的一些示例(用于简洁的 static_assert):

void doesNothing(){}
struct S
{
    int x{0};
};
int x = 1;
int y = 2;
S s;

static_assert(IS_LVALUE(x));
static_assert(IS_LVALUE(x+=y));
static_assert(IS_LVALUE("Hello world!"));
static_assert(IS_LVALUE(++x));

static_assert(IS_PRVALUE(1));
static_assert(IS_PRVALUE(x++));
static_assert(IS_PRVALUE(static_cast<double>(x)));
static_assert(IS_PRVALUE(std::string{}));
static_assert(IS_PRVALUE(throw std::exception()));
static_assert(IS_PRVALUE(doesNothing()));

static_assert(IS_XVALUE(std::move(s)));
// The next one doesn't work in gcc 8.2 but in gcc 9.1. Clang 7.0.0 and msvc 19.16 are doing fine.
static_assert(IS_XVALUE(S().x)); 

一旦你弄清楚了主要类别,混合类别就有点无聊了。

有关更多示例(和实验),请查看编译器资源管理器上的以下链接。不过,不要费心阅读程序集。我添加了很多编译器只是为了确保它适用于所有常见的编译器。

于 2019-02-14T16:10:33.580 回答
20

这些新类别与现有的右值和左值类别有何关系?

C++03 左值仍然是 C++11 左值,而 C++03 右值在 C++11 中称为右值。

于 2010-08-30T15:45:59.547 回答
17

这些是 C++ 委员会用来在 C++11 中定义移动语义的术语。这是故事

鉴于它们的精确定义、长长的规则列表或这个流行的图表,我发现很难理解这些术语:

价值类别传统图

在带有典型示例的维恩图上更容易:

价值类别维恩图

基本上:

  • 每个表达式都是左值或右值
  • lvalue必须复制,因为它有标识,所以以后可以使用
  • 右值可以移动,因为它是临时的(prvalue)或显式移动的(xvalue)

现在,好问题是,如果我们有两个正交属性(“具有身份”和“可以移动”),完成左值、xvalue 和 prvalue 的第四个类别是什么?那将是一个没有身份的表达式(因此以后无法访问)并且无法移动(需要复制其值)。这根本没用,所以没有命名。

于 2021-03-15T15:20:25.827 回答
14

上面优秀答案的一个附录,即使在我阅读了 Stroustrup 并认为我理解了右值/左值的区别之后,这一点也让我感到困惑。当你看到

int&& a = 3,

很容易将 阅读int&&为一种类型并得出结论它a是一个右值。它不是:

int&& a = 3;
int&& c = a; //error: cannot bind 'int' lvalue to 'int&&'
int& b = a; //compiles

a有一个名字并且事实上是一个左值。不要将;&&视为 ; 类型的一部分a。它只是告诉您a允许绑定的内容。

T&&这对于构造函数中的类型参数尤其重要。如果你写

Foo::Foo(T&& _t) : t{_t} {}

您将复制_tt. 你需要

Foo::Foo(T&& _t) : t{std::move(_t)} {}如果你想搬家。当我遗漏move!时,我的编译器会警告我吗?

于 2016-07-25T04:26:13.860 回答
6

这是我为我正在编写的一本高度可视化的 C++ 书籍制作的维恩图,我将在开发期间很快在leapub 上发布。

显示 C++ 中值类别之间关系的维恩图

其他答案用文字更详细,并显示类似的图表。但希望这个信息的呈现是相当完整的,并且对参考有用,此外。

我对这个主题的主要收获是表达式具有这两个属性:身份和可移动性。第一个涉及事物存在的“坚固性”。这很重要,因为允许并鼓励 C++ 抽象机通过优化来积极地更改和缩小代码,这意味着没有身份的东西可能只会在编译器的脑海中或寄存器中存在片刻,然后才会被践踏上。但是如果你回收它的内脏,这样的数据也保证不会引起问题,因为没有办法尝试使用它。因此,发明了移动语义以允许我们捕获对临时对象的引用,将它们升级为左值并延长它们的生命周期。

移动语义最初不仅仅是浪费地扔掉临时的东西,而是把它们送给别人,这样它们就可以被另一个人消耗掉。

当你想吃你朋友剩下的玉米面包时,不要浪费。

当你把你的玉米面包送出去时,你送给它的人现在拥有它。他们消费它。一旦你把它送出去,你不应该尝试吃或消化所说的玉米面包。也许那个玉米面包无论如何都被扔进了垃圾箱,但现在它却被扔到了他们的肚子里。它不再是你的了。

在 C++ 领域,“消耗”资源的想法意味着资源现在归我们所有,因此我们应该进行必要的清理,并确保不会在其他地方访问该对象。通常,这意味着借用胆量来创建新对象。我称之为“捐赠器官”。通常,我们谈论的是对象包含的指针或引用,或类似的东西,我们希望保留这些指针或引用,因为它们引用了我们程序中其他地方没有死亡的数据。

因此,您可以编写一个接受右值引用的函数重载,如果传入了一个临时(prvalue),那就是要调用的重载。在绑定到函数采用的右值引用时,将创建一个新的左值,从而延长临时变量的寿命,以便您可以在函数中使用它。

在某个时候,我们意识到我们经常在一个范围内完成了左值非临时数据,但想在另一个范围内蚕食。但它们不是右值,因此不会绑定到右值引用。所以我们做了std::move,这只是从左值到右值引用的一个花哨的转换。这样的数据是一个 xvalue:一个以前的左值现在就像一个临时的,所以它也可以被移动。

于 2021-09-09T20:55:50.267 回答