我读过lvalues
是“具有已定义存储位置的东西”。
而且文字和临时变量也不是左值,但没有给出这个陈述的理由。
是因为文字和临时变量没有定义存储位置?如果是的话,那么如果不在记忆中它们会在哪里居住?
我想在“定义的存储位置”中“定义”有一些意义,如果有(或没有)请告诉我。
而且文字和临时变量也不是左值,但没有给出这个陈述的理由。
对于除字符串文字之外的所有临时文字和文字都是如此。这些实际上是左值(将在下面解释)。
是因为文字和临时变量没有定义的存储位置?如果是的话,那么如果不在记忆中它们会在哪里居住?
是。字面上的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),因为标准说的是(2)。让我们关注(1)。
是因为文字和临时变量没有定义的存储位置?
这是一个不适合的简化。简化:文字和临时不是左值,因为修改它们没有意义1。
5++
是什么意思? rand() = 0
是什么意思?标准说临时文字和文字不是左值,所以那些例子是无效的。每个编译器开发人员都更开心。
1)您可以以修改临时的方式定义和使用用户定义的类型。这个临时表决直到评估完整表达。 FrançoisAndrieux在另一方面调用f(MyType{}.mutate())
和另一方面调用f(my_int + 1)
之间做了一个很好的类比。我认为简化仍然存在,因为MyType{}.mutate()
可以被视为MyType{}
的另一个临时性,像my_int + 1
可以被视为int
的另一个my_int
。这是所有语义和基于意见的。真正的答案是:(2)因为标准这样说。
在问题和其他答案中存在许多常见的误解;我的回答希望解决这个问题。
术语lvalue和rvalue是表达类别。它们是适用于表达式的术语。不是对象。 (有点令人困惑的是,表达类别的官方术语是“价值类别”!)
术语临时对象是指对象。这包括类类型的对象,以及内置类型的对象。术语临时(用作名词)是临时对象的缩写。有时,独立术语值用于指代内置类型的临时对象。这些术语适用于对象,而不适用于表达式。
与过去的标准相比,C ++ 17标准在对象术语中更加一致,例如见[conv.rval] / 1。它现在试图避免在表达式的上下文值之外说出值。
现在,为什么有不同的表达类别? 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;
创建的对象)都存在于内存中。
正如我在前面的回答中所描述的,程序由操纵对象的表达式组成。据说每个表达式指定或引用一个对象。
关于这个主题的其他答案或文章很常见的是,rvalue只能指定一个临时对象,或者更糟糕的是,rvalue是一个临时对象,或者一个临时对象是一个rvalue。表达式不是对象,它是源代码中用于操作对象的东西!
实际上,临时对象可以由左值或右值表达式指定;并且可以通过左值或右值表达式指定非临时对象。它们是不同的概念。
现在,有一个表达式类别规则,您无法将&
应用于右值类别的表达式。此规则和这些类别的目的是避免在销毁临时对象后使用临时对象的错误。例如:
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 ++是根据抽象机器定义的,其中临时对象也有存储和地址。我之前举了一个例子,说明如何打印临时对象的地址。
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;
。
这同样适用于没有会改变程序输出的副作用的类类型。例如。 std::string x = "foo"; std::cout << x;
可以优化到std::cout << "foo";
,即使左值x
表示在抽象机器中存储的对象。
lvalue
代表定位器值,表示占用内存中某些可识别位置的对象。
术语定位器值也使用here:
C
C编程语言遵循类似的分类,除了赋值的作用不再重要:C表达式分为“左值表达式”和其他(函数和非对象值),其中“左值”表示标识对象,“定位器值”[4]。
所有不是lvalue
的东西都是排除了rvalue
。每个表达都是lavalue
或rvalue
。
最初lvalue
术语在C中用于表示可以保留在赋值运算符左侧的值。然而,随着const
关键字的改变。并非所有lvalues
都可以分配给。那些可以被称为modifiable lvalues
。
而且文字和临时变量也不是左值,但没有给出这个陈述的理由。
根据this answer文字在某些情况下可以是lvalues
。
rvalue
,因为它们具有已知的大小,并且很可能直接嵌入到给定硬件体系结构上的机器命令中。什么是5
的记忆位置?lvalues
,因为它们具有不可预测的大小,并且除了作为内存中的对象之外没有其他方式来表示它们。lvalue
可以转换为rvalue
。例如,在以下说明中
int a =5;
int b = 3;
int c = a+b;
运营商+
需要两个rvalues
。所以a
和b
在被总结之前被转换为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中,rvalues引用与移动构造函数和移动赋值运算符有关。
你可以在this clear and well-explained post找到更多细节。
如果不在记忆中他们住在哪里?
当然他们居住在记忆中*,没有办法绕过它。问题是,你的程序能否确定它们在内存中的确切位置。换句话说,您的程序是否允许获取相关内容的地址。
在一个简单的例子中,a = 5
的值为5,或者表示值为5的指令,在内存中的某处。但是,你不能取五个地址,因为int *p = &5
是非法的。
请注意,字符串文字是“非左值”规则的例外,因为const char *p = "hello"
生成字符串文字的地址。
short a; a = 0xFF00
could be represented as a an assignment of 0xFF
in the upper octet, and clearing out the lower octet in memory.