右值引用:究竟什么是“临时”对象,它们的范围是什么,它们存储在哪里?
阅读一些文章,rvalues总是被定义为“临时”对象,例如Animal()
,其中Animal
是一个类,或者一些文字,例如10
。
然而,rvalues /“临时”对象的正式定义是什么?
new Animal()
是否也被视为“临时”对象?或者它只是堆栈上的值,如Animal()
和存储在代码中的文字?
此外,这些“临时”对象存储在哪里,它们的范围是什么,这些值的右值引用有效期是多久?
答案 0 :(得分:10)
首先,重要的是不要混淆术语" rvalue"和"临时对象"。它们的含义截然不同。
临时对象没有存储持续时间。相反,它们具有特定于临时对象的生命周期规则。这些可以在C ++标准的[class.temporary]部分找到;有一个summary on cppreference,其中还包含一个表达式列表,用于创建临时对象。
在实践中,我希望编译器可以优化对象,或者将其存储在与存储自动对象相同的位置。
请注意"临时对象"仅指类类型的对象。内置类型的等效项称为值。 (不是"临时值")。事实上,术语"值"包括内置类型和临时对象的值。
A"价值"对于prvalue,xvalue,rvalue来说是一个完全独立的想法。拼写的相似性是不幸的。
价值观没有范围。 范围是名称的属性。在许多情况下,名称的范围与其命名的对象或值的生命周期一致,但并非总是如此。
术语 rvalue ,左值等是表达式的值类别。 这些描述表达式,而不是值或对象。
每个表达式都有值类别。此外,除void
类型的表达式外,每个表达式都有值。这是两件不同的事情。 (表达式的值具有非引用类型。)
值类别 rvalue 的表达式可以指定临时对象,非临时对象或内置类型的值。
创建临时对象的表达式都具有值类别 prvalue ,但是可以形成具有 lvalue 类别的表达式同一个临时对象。例如:
const std::string &v = std::string("hello");
在这种情况下,v
是左值,但它指定了一个临时对象。此临时对象的生命周期与v
的生存期匹配,如前面的cppreference链接中所述。
Link to further reading about value categories
右值引用是一个只能绑定到值类别 rvalue 的表达式的引用。 (包括 prvalue 和 xvalue )。名称中的 rvalue 一词是指它与绑定的内容,而不是它自己的值类别。
所有命名引用实际上都有 lvalue 类别。绑定后,右值引用和左值引用之间的行为没有区别。
std::string&& rref = std::string("hello");
rref
具有值类别 lvalue ,它指定一个临时对象。此示例与前一个示例非常相似,但此时临时对象不是const。
另一个例子:
std::string s1("hello");
std::string&& rref = std::move(s1);
std::string& lref = s1;
在这种情况下,rref
是左值,它指定一个非临时对象。此外,lref
和rref
(甚至s1
!)都与此无法区分,具体而言decltype
的结果除外。
答案 1 :(得分:4)
有两件事需要关注。首先,有语言的观点。语言规范,例如C ++标准,不讨论诸如CPU寄存器,高速缓存一致性,堆栈(在汇编意义上)等等......然后,有一个真正的机器&# 39;观点。 Instruction set architectures(ISA),例如由Intel manuals定义的那个,确实关注这个问题。当然,这是因为可移植性和抽象性。 C ++没有充分的理由依赖于x86特定的细节,但很多不好的细节。我的意思是,假设HelloWorld.cpp
只为你的特定Core i7模型编译而没有任何理由!同时,您有时需要CPU特定的东西。例如,您如何以便携方式发出CLI指令?我们有不同的语言,因为我们需要解决不同的任务,并且我们有不同的ISA,因为我们需要不同的意味着来解决它们。有一个很好的理由可以解释为什么你的智能手机没有使用英特尔CPU,或者为什么Linux内核是用C语言写的,而不是...... Brainfuck。
现在,从语言的角度来看," rvalue"是一个临时值,其生命周期以它在其中计算的表达式结束。
实际上,rvalues的实现方式与自动变量相同,即通过将其值存储在堆栈中,或者如果编译器认为它适合,则执行寄存器。在自动变量的情况下,编译器可以将其存储在寄存器中,如果它的地址是在程序中的某个地方,因为寄存器没有"地址"。但是,如果从未采用其地址,并且不涉及volatile
内容,则编译器的优化器可以将该变量放入寄存器中以进行优化。对于左值,这种情况总是如此,因为您无法获取右值的地址。从语言的角度来看,他们没有一个(注意:我在这里使用旧的C术语;请参阅注释以获取详细信息,因为有方法这里有太多的C ++ 11陷阱需要注释。这是一些正常工作所必需的。例如,cdecl
要求在EAX
寄存器中返回小值。因此,所有函数调用必须求值为rvalue(为了简单起见,将引用视为指针),因为你不能使用寄存器的地址,因为它们没有!
还有" life"的概念。从语言的角度来看,一旦某个对象的生命周期结束,它就不再是时代。什么时候开始"和"结束"取决于对象的分配方式:
new
表达式明确地开始,并通过delete
语句显式结束。这种机制允许它们在原始范围内存活(例如:return new int;
)。main()
之前开始,并在main()
退出后结束。构造和破坏分别涉及对象的生命周期"开始"和"结束"。
从真实机器的角度来看,位只是位,周期。没有"对象"但存储器单元和CPU寄存器中的位和字节。像int
这样的东西,即POD type,"结束其生命周期"转化为什么都不做。对于非平凡的可破坏的非POD类型,必须在正确的时刻调用析构函数。但是,曾经包含"对象的内存/寄存器"还在那里。它恰好可以被其他东西重用。
新的Animal()也被认为是"临时"宾语?或者它只是堆栈上的值,如Animal()和存储在代码中的文字?
new Animal()
在堆中为Animal
分配内存,构造它,整个表达式计算为Animal*
。这样的表达式本身就是一个右值,因为你不能说&(new Animal())
之类的东西。但是,表达式计算为指针,不?这样的指针指向左值,因为你可以说&(*(new Animal()))
之类的东西(但会泄漏)。我的意思是,如果有一个包含其地址的指针,那么有一个地址,不是吗?
此外,这些"临时"存储的对象,它们的范围是什么,以及这些值的右值引用有效多长时间?
如上所述,"临时对象"的范围是包含它的表达式的范围。例如,在表达式a(b * c)
中(假设a
是一个将右值引用作为其单个参数的函数),b * c
是一个右值,其范围在包含它的表达式后结束,即,A(...)
,进行评估。之后,函数a
可能以某种方式从其参数中创建的所有剩余rvalue引用都悬空,并将导致程序执行有趣的操作。换言之,只要您不滥用std::move
或使用右值进行其他伏都教,右值引用在您预期的情况下有效。