为什么文字和临时变量不是左值?

Why are literals and temporary variables not lvalues?

我读到 lvalues 是 "things with a defined storage location"。

此外,文字和临时变量不是左值,但没有给出此声明的理由。

是不是字面量和临时变量没有定义存储位置?如果是,那么如果不在内存中,它们将驻留在何处?

我想 "defined" 在 "defined storage location" 中有一些意义,如果有(或没有)请告诉我。

Where do they reside if not in memory?

当然是驻留在内存中*,没法绕过。问题是,您的程序能否确定它们在内存中的确切位置。换句话说,你的程序是否允许获取有问题的地址。

在一个简单的例子中,a = 5 值 5,或表示赋值 5 的指令,在内存中的某处。但是,你不能取五的地址,因为int *p = &5是非法的。

请注意,字符串文字是 "not an lvalue" 规则的一个例外,因为 const char *p = "hello" 生成字符串文字的地址。


* 不过不一定是data内存。事实上,它们甚至可能不会在程序存储器中表示为常量:例如,赋值 short a; a = 0xFF00 可以表示为高位八位组中 0xFF 的赋值,并清除低位内存中的八位字节。

And also that literals and temporaries variables are not lvalues, but no reason is given for this statement.

除字符串文字外,所有临时变量和文字都是如此。这些实际上是左值(在下面解释)。

Is it because literals and temporaries variables do not have defined storage location? If yes, then where do they reside if not in memory?

是的。文字 2 实际上并不存在;它只是源代码中的一个值。因为它是一个值,而不是一个对象,所以它不需要关联任何内存。它可以硬编码到编译器创建的程序集中,也可以将其放在某个地方,但由于不必如此,您所能做的就是将其视为纯值,而不是对象。

不过有一个例外,那就是字符串文字。那些实际上有存储空间,因为字符串文字是 const char[N] 的数组。您可以获取字符串文字的地址,并且字符串文字可以衰减为指针,因此它是一个左值,即使它没有名称。

临时变量也是右值。即使它们作为对象存在,它们的存储位置也是短暂的。它们只会持续到它们所在的完整表达式结束。您不能使用它们的地址,它们也没有名字。它们甚至可能不存在:例如,在

Foo a = Foo();

可以删除Foo()并将代码语义转换为

Foo a(); // you can't actually do this since it declares a function with that signature.

所以现在优化代码中甚至没有临时对象。

Why are literals and temporary variables not lvalues?

我有两个答案:因为它没有意义 (1) 并且因为标准是这么说的 (2)。让我们关注(1)。

Is it because literals and temporaries variables do not have defined storage location?

这是一个不适合此处的简化。一个简化:文字和临时值不是左值,因为修改它们没有意义1.

5++是什么意思? rand() = 0 是什么意思? 标准说临时变量和文字不是左值,所以这些例子是无效的。每个编译器开发人员都更快乐。


1) 您可以定义和使用用户定义的类型,使临时修改有意义。这个临时对象会一直存在,直到对完整表达式进行评估。 François Andrieux 在一方面调用 f(MyType{}.mutate()) 和另一方面调用 f(my_int + 1) 之间做了一个很好的类比。我认为简化仍然有效,因为 MyType{}.mutate() 可以被 视为 另一个临时性的 MyType{} 就像 my_int + 1 可以被 视为as 另一个 intmy_int 一样。这都是基于语义和意见的。真正的答案是:(2) 因为标准是这么说的。

lvalue 代表 定位器值 并表示一个对象在内存中占据一些可识别的位置。

术语定位器值也被使用here:

C

The C programming language followed a similar taxonomy, except that the role of assignment was no longer significant: C expressions are categorized between "lvalue expressions" and others (functions and non-object values), where "lvalue" means an expression that identifies an object, a "locator value"[4].

所有不是 lvalue 的东西都被排除在 rvalue 之外。每个表达式都是 lavaluervalue.

最初 lvalue 术语在 C 中用于指示可以保留在赋值运算符左侧的值。然而,随着 const 键的使用,这发生了变化。并非所有 lvalues 都可以分配给。能者称modifiable lvalues

And also that literals and temporaries variables are not lvalues, but no reason is given for this statement.

根据 this answer,在某些情况下,文字可以是 lvalues

  • 标量类型的文字rvalue,因为它们的大小已知并且很可能直接嵌入到给定硬件架构上的机器命令中。 5 的内存位置是什么?
  • 相反,奇怪的是,字符串文字lvalues,因为它们的大小不可预测,除了作为内存中的对象之外,没有其他方法可以表示它们.

lvalue 可以转换为 rvalue。例如在下面的指令中

int a =5;
int b = 3;
int c = a+b;

运算符+需要两个rvalues。所以 ab 在求和之前被转换为 rvalues。转换的另一个例子:

int c = 6;
&c = 4; //ERROR: &c is an rvalue

相反 您不能将 rvalue 转换为 lvalue

但是您可以rvalue生成有效的lvalue,例如:

int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10;   // OK: p + 1 is an rvalue, but *(p + 1) is an lvalue

在 C++11 中,右值引用与移动构造函数和移动赋值运算符相关。

您可以在 this clear and well-explained post 中找到更多详细信息。

问题和其他答案中存在很多常见的误解;我的回答希望能解决这个问题。

左值右值表达式类别。它们是适用于表达式的术语。不是对象。 (有点令人困惑,表达式类别的官方术语是 "value categories" !)

术语临时对象指的是对象。这包括 class 类型的对象,以及内置类型的对象。术语 temporary(用作名词)是 temporary object 的缩写。有时,独立术语 value 用于指代内置类型的临时对象。这些术语适用于对象,不适用于表达式。

C++17 标准在对象术语方面比过去的标准更加一致,例如参见 [conv.rval]/1。它现在试图避免说 value,而不是在 value of an expression 的上下文中。


现在,为什么会有不同的表达类别? C++ 程序由一组表达式组成,这些表达式通过运算符相互连接以构成更大的表达式;并适合声明性结构的框架。这些表达式创建、销毁对象并对其进行其他操作。用 C++ 编程可以描述为使用表达式对对象执行操作。

存在表达式类别的原因是为了提供一个框架,以便使用表达式来表达程序员想要的操作。例如,早在 C 时代(可能更早),语言设计者认为 3 = 5; 作为程序的一部分没有任何意义,因此决定限制可以出现在左侧的表达式类型 - = 的手边,如果未遵守此限制,则让编译器报告错误。

术语 lvalue 起源于那些日子,虽然现在随着 C++ 的发展,有大量的表达式和上下文,其中表达式类别是有用的,而不仅仅是左 -赋值运算符的手边。

这是一些有效的 C++ 代码:std::string("3") = std::string("5");。这在概念上与 3 = 5; 没有区别,但它是允许的。效果是创建了一个类型为std::string、内容为"3"的临时对象,然后修改该临时对象的内容为"5",然后销毁该临时对象。该语言本可以设计为代码 3 = 5; 指定一系列类似的事件(但事实并非如此)。


为什么 string 示例合法而 int 示例不合法?

每个表达式都必须有一个类别。表达式的类别起初似乎没有明显的原因,但是语言的设计者根据他们认为表达有用的概念和没有表达的概念,为每个表达式赋予了一个类别。

已经确定 3 = 5; 中的事件顺序如上所述不是任何人想要做的事情,如果有人确实写了这样的东西那么他们可能犯了错误并且有其他意思,所以编译器应该通过给出错误信息来提供帮助。

现在,同样的逻辑可能会得出结论,std::string("3") = std::string("5") 也不是任何人都愿意做的事情。然而,另一个论点是对于某些其他 class 类型,T(foo) = x; 实际上可能是一个有价值的操作,例如因为 T 可能有一个做某事的析构函数。决定禁止这种用法对程序员的意图来说弊大于利。 (这是否是一个好的决定值得商榷;see this question 供讨论)。


现在我们离最终解决您的问题越来越近了:)

是否有关联的内存或存储位置不再是表达式类别的基本原理。在抽象机中(下面对此有更多解释),每个临时对象(包括3x = 3;中创建的对象)都存在于内存中。

如我前面的回答所述,程序由操作对象的表达式组成。据说每个表达式 指定 引用 一个对象。

关于此主题的其他答案或文章经常错误地声称右值只能指定一个临时对象,或者更糟糕的是,右值 一个临时对象,或者临时对象是右值。表达式不是对象,它是出现在源代码中用于操作对象的东西!

事实上临时对象可以用左值或右值表达式指定;并且非临时对象可以由左值或右值表达式指定。它们是不同的概念。

现在,有一个表达式类别规则,您不能将 & 应用于右值类别的表达式。此规则和这些类别的目的是避免在临时对象被销毁后使用的错误。例如:

int *p = &5;    // not allowed due to category rules
*p = 6;         // oops, dangling pointer

但你可以解决这个问题:

template<typename T> auto f(T&&t) -> T& { return t; }
// ...
int *p = f(5); // Allowed
*p = 6;        // Oops, dangling pointer, no compiler error message.

在后面的代码中,f(5)*p 都是指定临时对象的左值。这是表达类别规则存在的一个很好的例子;如果遵循规则而不使用棘手的解决方法,那么我们会在尝试通过悬垂指针写入的代码中出错。

请注意,您也可以使用此 f 来查找临时对象的内存地址,例如std::cout << &f(5);


综上所述,您实际提出的问题都错误地将表达式与对象混为一谈。所以从这个意义上说,它们不是问题。临时变量不是左值,因为对象不是表达式。

一个有效但相关的问题是:"Why is the expression that creates a temporary object an rvalue (as opposed to being an lvalue?)"

答案如上文所述:将其设为左值会增加创建悬垂指针或悬垂引用的风险;和 3 = 5; 中一样,会增加指定程序员可能不打算执行的冗余操作的风险。

我再次重申,表达式类别是帮助程序员表现力的设计决策;与内存或存储位置无关。


最后,到抽象机和假设规则。 C++ 是根据抽象机定义的,其中临时对象也有存储和地址。我之前给出了一个如何打印临时对象地址的例子。

as-if 规则 表示编译器生成的实际可执行文件的输出必须仅匹配抽象机的输出。可执行文件实际上不必以与抽象机相同的方式工作,它只需要产生相同的结果。

所以对于像 x = 5; 这样的代码,即使值 5 的临时对象在抽象机中有一个内存位置;编译器不必在真机上分配物理存储空间。它只需要确保 x 最终将 5 存储在其中,并且有更简单的方法可以做到这一点,不需要创建额外的存储空间。

as-if 规则适用于程序中的所有内容,即使我这里的示例仅涉及临时对象。一个非临时对象同样可以被优化掉,例如int x; int y = 5; x = y; // other code that doesn't use y 可以更改为 int x = 5;

这同样适用于 class 类型,没有会改变程序输出的副作用。例如。 std::string x = "foo"; std::cout << x; 可以优化为 std::cout << "foo";,即使左值 x 表示抽象机中具有存储空间的对象。