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

时间:2019-02-12 15:17:39

标签: c++ c++11 rvalue lvalue

我已经读到lvalues是“具有定义的存储位置的东西”。

而且字面量和临时变量不是左值,但没有给出此语句的原因。

是因为文字和临时变量没有定义的存储位置吗?如果是,那么如果不在内存中,它们将驻留在哪里?

我想在“定义的存储位置”中对“定义的”有一定意义,如果有(或没有),请告诉我。

5 个答案:

答案 0 :(得分:20)

  

而且字面量和临时变量不是左值,但没有给出此语句的原因。

除了字符串文字外,所有临时变量和文字都适用。这些实际上是左值(在下面说明)。

  

是因为文字和临时变量没有定义的存储位置吗?如果是,那么如果不在内存中,它们将驻留在哪里?

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

但是有一个免除条款,那就是字符串文字。因为字符串文字是const char[N]的数组,所以它们实际上有存储空间。您可以使用字符串文字的地址,并且字符串文字可以衰减为指针,因此即使没有名称,它也是一个左值。

临时也是右值。即使它们作为对象存在,它们的存储位置也是短暂的。它们只持续到它们所在的完整表达式的末尾。不允许您输入其地址,并且它们也没有名称。它们甚至可能不存在:例如,在

Foo a = Foo();

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

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

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

答案 1 :(得分:9)

  

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

我有两个答案:因为这没有道理(1),而且因为标准这么说(2)。让我们专注于(1)。

  

是因为文字和临时变量没有定义的存储位置吗?

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

5++是什么意思? rand() = 0的含义是什么?该标准指出,临时变量和文字不是左值,因此这些示例无效。每个编译器开发人员都更加快乐。


1)您可以通过修改临时变量的方式定义和使用用户定义的类型。该临时选项将一直存在,直到对完整表达式进行评估为止。弗朗索瓦·安德列(FrançoisAndrieux)在一方面调用f(MyType{}.mutate())和另一方面调用f(my_int + 1)之间做出了一个很好的类比。我认为简化仍然存在,因为MyType{}.mutate()可以视为,而另一个临时{@ {1}}是,例如MyType{}可以视为另一个my_int + 1int一样。这都是基于语义和观点的。真正的答案是:(2)因为标准是这样。

答案 2 :(得分:7)

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

术语左值右值表达式类别。它们是适用于表达式的术语。不反对。 (有些混淆,表达式类别的正式术语是“值类别”!)

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

与过去的标准相比,C ++ 17标准在对象术语上更加一致,例如参见[conv.rval] / 1。现在,它尝试避免在上下文的中说出 value


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

存在表达式类别的原因是提供一个框架,用于使用表达式来表示程序员想要的操作。例如,回到C天(可能更早),语言设计师认为3 = 5;作为程序的一部分没有任何意义,因此决定限制可以在左侧显示的表达式类型, =的另一侧,如果不遵守此限制,则让编译器报告错误。

术语“左值” 起源于那个年代,尽管现在随着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")并不是任何人都想做的。但是,另一个论点是,对于某些其他类类型,T(foo) = x;实际上可能是值得进行的操作,例如因为T可能有一个执行某些操作的析构函数。决定禁止这种用法可能对程序员的意图有害,而不是有益。 (无论这是一个好的决定还是值得商;的; see this question进行讨论)。


现在我们越来越接近,终于可以解决您的问题了:)

是否有内存或关联的存储位置不再是表达式类别的理由。在抽象机(下面对此有更多说明)中,每个临时对象(包括3中的x = 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);


总而言之,您实际上提出的所有问题都会错误地将表达式与对象混淆。因此,从这个意义上讲,它们不是问题。临时对象不是左值,因为对象不是表达式。

一个有效但相关的问题是:“为什么创建一个临时对象的表达式是右值(而不是左值?)”

答案如前所述:将其设置为左值将增加创建悬挂指针或悬挂引用的风险;并且像3 = 5;中那样,将增加指定程序员可能不希望的冗余操作的风险。

我再说一遍,表达类别是一个有助于程序员表达的设计决策;与内存或存储位置无关。


最后,使用抽象机器和 as-if规则。 C ++是根据抽象机定义的,其中临时对象也具有存储和地址。我在前面给出了一个示例,该示例如何显示临时对象的地址。

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

因此对于x = 5;这样的代码,即使值为5的临时对象在抽象机中也有存储位置;编译器不必在实际计算机上分配物理存储。只需确保x最终存储了5,并且可以通过更简单的方法来完成此操作,而无需创建额外的存储空间。

按条件规则适用于程序中的所有内容,即使此处的示例仅涉及临时对象。同样可以很好地优化非临时对象,例如int x; int y = 5; x = y; // other code that doesn't use y可以更改为int x = 5;

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

答案 3 :(得分:4)

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

术语定位器值也用于here

  

C

     

C编程语言遵循类似的分类法,除了   赋值的作用不再重要:C表达式是   在“左值表达式”和其他(函数和   非对象值),其中“ lvalue”表示可标识   一个对象,一个“定位器值” [4]。

不是lvalue的所有内容都被排除为rvalue。每个表达式都是lavaluervalue

C中最初使用lvalue项来表示可以保留在赋值运算符左侧的值。但是,使用const键盘进行了更改。并非所有lvalues都可以分配给它。可以称为modifiable lvalues的那些。

  

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

在某些情况下,根据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

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

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中,右值引用与move构造函数和move赋值运算符相关。

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

答案 4 :(得分:3)

  

如果不在内存中,它们将驻留在哪里?

当然它们驻留在内存中 * ,没有办法解决。问题是,您的程序可以确定它们在内存中的确切位置。换句话说,您的程序是否允许使用有问题的地址。

在一个简单的示例a = 5中,值5或代表值5的赋值指令位于内存中的某个位置。但是,您不能使用地址5,因为int *p = &5是非法的。

请注意,字符串文字是“非左值”规则的例外,因为const char *p = "hello"会产生字符串文字的地址。


* 但是,它不一定是 data 内存。实际上,它们甚至可能无法在程序存储器中表示为常量:例如,分配short a; a = 0xFF00可以表示为高八位字节中的0xFF分配,并清除较低的八位字节八位字节在内存中。