C 中的 LValue 和 RValue
背景
不少你谁是关于阅读这篇文章,可能会寻求澄清曾经被认为是基本的:右值都可以在赋值运算符的右边弹出一样的东西,并且左值分别是左侧属于一样的东西或正确的赋值运算符。毕竟,这就是 k&R 区分某些表达式与其他表达式的方式:
An object is a named region of storage; an lvalue is an expression referring to an object. An obvious example of an lvalue expression is an identifier with suitable type and storage class. There are operators that yield lvalues: for example, if E is an expression of pointer type, then *E is an lvalue expression referring to the object to which E points. The name “lvalue” comes from the assignment expression E1 = E2 in which the left operand E 1 must be an lvalue expression. The discussion of each operator specifies whether it expects lvalue operands and whether it yields an lvalue.
不幸的是,这种简单的方法现在是黑暗时代顽固的纪念品。当我们有足够的勇气查阅最新的规范时,我们在第 3.10 段中提出了以下分类法:
expression
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue
谷歌搜索比规范本身更易读的说明,搜索结果进入左值引用和右值引用之间的区别,移动语义的细微差别,……所有这些实际上需要这种混乱的基本概念层次结构的高级功能第一名。
好吧,这篇文章提供了一些完全不同的东西:它会试图让那些第一次看到这些术语的人理解所有这些,而不需要通过情绪增强的方法来理解这一切……我们甚至可以提供第一个建议我们需要牢记:
忘记赋值运算符左侧和右侧的赋值和事物。
这棵语义标签树中最大的挑战是神秘的xvalue 。我们不必理解xvalues ,那是为了势利小人。我们可以限制自己理解lvalues和prvalues 。如果您已经了解xvalues ,您可以快速润色您的金色“精英 C++ 程序员”牌匾,并查找有关如何充分利用这些xvalues 的不同文章。对于我们其他人,我们可以将当前段落重新表述为第二个建议:
专注于理解各种表达式中的左值和右值。
左值
我们正在讨论表达式的语法和语义,并且赋值被巧妙地隐藏在此类表达式的 BNF(Backus-Naur-Form)中。这就是为什么第二个建议建议忘记分配。因为规范仍然很清楚什么是左值!但是,与其解读冗长的描述,不如提供一些源代码示例:
// Designates an object
就是这样(或多或少)!好吧,我们可以弄清楚类是如何类型的,类实例也是对象,并从中观察到实例和成员的引用和指针如何也是对象;然而,这正是那种让我们不知所措的解释,以至于它掩盖了基础!至此,我们有一个典型的例子,用于lvalues 的4 种不同表现形式。规范没有规定任何关于它只属于赋值运算符的左侧或右侧的限制!左值是最终在内存中定位对象的表达式。
Hence the much more appropriate description of lvalue as a “locator-value”.
在这一点上,我们必须承认,我们在一个左悄悄在初始化表达式:LV1不只是在声明中的左值在声明!即使当lv1用于初始化lv2 引用(一个引用应该被初始化,总是), lv1仍然是一个左值!
说明左值使用的最好方法是将其用作结果存储的定位器,以及数据输入的定位器;来看看他们的行动:
CPP
// CPP program to illustrate the concept of lvalue
#include
using namespace std;
// §3.10.1
// An lvalue designates a function or an object
// An lvalue is an expression whose
// address can be taken:
// essentially a locator value
int lv1{ 42 }; // Object
int& lv2{ lv1 }; // Reveference to Object
int* lv3{ &lv1 }; // Pointer to Object
int& lv4()
{
// Function returning Lvalue Reference
return lv1;
}
int main()
{
// Examine the lvalue expressions
cout << lv1 << "\tObject" << endl;
cout << lv2 << "\tReference" << endl;
cout << lv3 << "\tPointer (object)" << endl;
cout << *lv3 << "\tPointer (value=locator)" << endl;
cout << lv4() << "\tFunction provided reference" << endl;
// Use the lvalue as the target
// of an assignment expression
lv1 = 10;
cout << lv4() << "\tAssignment to object locator" << endl;
lv2 = 20;
cout << lv4() << "\tAssignment to reference locator" << endl;
*lv3 = 30;
cout << lv4() << "\tAssignment to pointer locator" << endl;
// Use the lvalue on the right hand side
// of an assignment expression
// Note that according to the specification,
// those lvalues will first
// be converted to prvalues! But
// in the expression below, they are
// still lvalues...
lv4() = lv1 + lv2 + *lv3;
cout << lv1 << "\tAssignment to reference locator (from function)\n"
"\t\tresult obtained from lvalues to the right of\n"
"\t\tassignment operator"
<< endl;
return 0;
}
CPP
// CPP program to illustrate glvalue
#include
using namespace std;
// §3.10.1
// An rvalue is an xvalue, a temporary object (§12.2),
// or a value not associated with an object
// A prvalue is an rvalue that is NOT an xvalue
// When a glvalue appears in a context
// where a prvalue is expected,
// the glvalue is converted to a prvalue
int prv1{ 42 }; // Value
constexpr int lv1{ 42 };
int prv2{ lv1 }; // Expression (lvalue)
constexpr int f1(int x)
{
return 6 * x;
}
int prv3{ f1(7) }; // Expression (function return value)
int prv4{ (lv1 + f1(7)) / 2 }; // Expression (temporary object)
int main()
{
// Print out the prvalues used
// in the initializations
cout << prv1 << " Value" << endl;
cout << prv2 << " Expression: lvalue" << endl;
cout << prv3 << " Expression: function return value" << endl;
cout << prv4 << " Expression: temporary object" << endl;
return 0;
}
CPP
#include
using namespace std;
// §8.3.2
// References are either form of:
// T& D lvalue reference
// T&& D rvalue reference
// They are distinct types (differentiating overloaded functions)
// §8.5.3
// The initializer of an rvalue reference shall not be an lvalue
// lvalue references
const int& lvr1{ 42 }; // value
int lv1{ 0 };
int& lvr2{ lv1 }; // lvalue (non-const)
constexpr int lv2{ 42 };
const int& lvr3{ lv2 }; // lvalue (const)
constexpr int f1(int x)
{
return 6 * x;
}
const int& lvr4{ f1(7) }; // Function return value
const int& lvr5{ (lv1 + f1(7)) / 2 }; // expression
// rvalue references
const int&& rvr1{ 42 }; // value
// Enable next two statements to reveal compiler error
#if 0
int&& rvr2 {lv1}; // lvalue (non-const)
const int&& rvr3 {lv2}; // lvalue (const)
#else
int&& rvr2{ static_cast(lv1) }; // rvalue (non-const)
const int&& rvr3{ static_cast(lv2) }; // rvalue (const)
#endif
const int&& rvr4{ f1(7) }; // Function return value
const int&& rvr5{ (lv1 + f1(7)) / 2 }; // expression
int main()
{
lv1 = 42;
// Print out the references
cout << lvr1 << " Value" << endl;
cout << lvr2 << " lvalue (non-const)" << endl;
cout << lvr3 << " lvalue (const)" << endl;
cout << lvr4 << " Function return value" << endl;
cout << lvr5 << " Expression (temporary object)" << endl;
cout << rvr1 << " Value" << endl;
cout << rvr2 << " rvalue (const)" << endl;
cout << rvr3 << " rvalue (non-const)" << endl;
cout << rvr4 << " Function return value" << endl;
cout << rvr5 << " Expression (temporary object)" << endl;
return 0;
}
CPP
#include
using namespace std;
// §12
// Special member functions
// . §12.1 Constructor
// . §12.8 Copy/Move
// - §12/1 Copy/Move Constructor
// - §13.5.3 Copy/Move Assignment Operator
struct T {
int _v1;
int _v2;
int _v3;
friend std::ostream& operator<<(std::ostream& os, const T& p)
{
return os << "[ " << p._v1 << " | " << p._v2 << " | " << p._v3 << " ]";
}
};
struct S {
S() // Constructor
{
cout << "Constructing instance of S" << endl;
_t = new T{ 1, 2, 3 };
}
S(T& t) // Constructor
{
cout << "Initializing instance of S" << endl;
_t = new T{ t };
}
S(const S& that) // Copy Constructor
{
cout << "Copying instance of S" << endl;
_t = new T;
*_t = *(that._t); // Deep copy
}
S& operator=(const S& that) // Copy Assignment Operator
{
cout << "Assigning instance of S" << endl;
*_t = *(that._t); // Deep copy
return *this;
}
S(S&& that) // Move Constructor
{
cout << "Moving instance of S" << endl;
_t = that._t; // Move resources
that._t = nullptr; // Reset source (protect)
}
S& operator=(S&& that) // Move Assignment Operator
{
cout << "Move-assigning instance of S" << endl;
_t = that._t; // Move resources
that._t = nullptr; // Reset source (protect)
return *this;
}
T* _t;
};
int main()
{
T t1{ 41, 42, 43 };
cout << t1 << " Initializer" << endl;
S s1{ t1 };
cout << s1._t << " : " << *(s1._t) << " Initialized" << endl;
S s2{ s1 };
cout << s2._t << " : " << *(s2._t) << " Copy Constructed" << endl;
S s3;
cout << s3._t << " : " << *(s3._t) << " Default Constructed" << endl;
s3 = s2;
cout << s3._t << " : " << *(s3._t) << " Copy Assigned" << endl;
S s4{ static_cast(s3) };
cout << s4._t << " : " << *(s4._t) << " Move Constructed" << endl;
s2 = std::move(s4);
cout << s2._t << " : " << *(s2._t) << " Move Assigned" << endl;
return 0;
}
42 Object
42 Reference
0x602070 Pointer (object)
42 Pointer (value=locator)
42 Function provided reference
10 Assignment to object locator
20 Assignment to reference locator
30 Assignment to pointer locator
90 Assignment to reference locator (from function)
result obtained from lvalues to the right of
assignment operator
右值
我们现在跳过更复杂的右值。在前面提到的黑暗时代,它们都是微不足道的。现在它们包括神秘的听起来xvalues !我们想忽略那些xvalues ,这正是prvalue的定义让我们做的:
一个prvalue是一个右值不是一个x值。
或者少一点混淆:
A prvalue represents a direct value.
这在初始化程序中最为明显:
但是,另一种选择是使用左值进行初始化:
这里发生了什么!这应该很简单,左值怎么可能是纯右值???在规范中,第 3.10.2 节有一句救命的句子:
Whenever a glvalue appears in a context where a prvalue is expected, the glvalue is converted to a prvalue.
让我们忽略了一个事实,即glvalue不是别的,只是左值或x值。我们已经在这个解释中禁止了xvalues 。因此:我们怎样才能从左值RV2值(prvalue)?通过转换(评估)它!
我们可以让它更有趣:
我们现在有一个函数f1() ,它返回一个值。该规范确实提供了引入临时变量 ( lvalue ) 的情况,然后在需要时将其转换为prvalue 。假装这正在发生:
对于更复杂的表达式,也有类似的解释:
现在小心!右值不是对象,也不是函数。右值是最终使用的:
- 字面量的值(与任何对象无关)。
- 函数的返回值(不涉及到任何对象,除非我们指望用于返回值的临时对象)。
- 保存表达式计算结果所需的临时对象的值。
对于通过执行编译器学习的人:
CPP
// CPP program to illustrate glvalue
#include
using namespace std;
// §3.10.1
// An rvalue is an xvalue, a temporary object (§12.2),
// or a value not associated with an object
// A prvalue is an rvalue that is NOT an xvalue
// When a glvalue appears in a context
// where a prvalue is expected,
// the glvalue is converted to a prvalue
int prv1{ 42 }; // Value
constexpr int lv1{ 42 };
int prv2{ lv1 }; // Expression (lvalue)
constexpr int f1(int x)
{
return 6 * x;
}
int prv3{ f1(7) }; // Expression (function return value)
int prv4{ (lv1 + f1(7)) / 2 }; // Expression (temporary object)
int main()
{
// Print out the prvalues used
// in the initializations
cout << prv1 << " Value" << endl;
cout << prv2 << " Expression: lvalue" << endl;
cout << prv3 << " Expression: function return value" << endl;
cout << prv4 << " Expression: temporary object" << endl;
return 0;
}
42 Value
42 Expression: lvalue
42 Expression: function return value
42 Expression: temporary object
值
等等:我们不是要讨论xvalues吗?!那么,在这一点上,我们已经了解到,左值和prvalues是真的没有那么难毕竟。几乎是任何有理智的人所期望的。我们不希望通过阅读这一切的文字很失望,只是要确认左值涉及的可定位对象,prvalues参考一些实际值。因此,这是一个惊喜:我们不妨也涵盖xvalues ,然后我们就完成了并理解了所有这些!
不过,我们需要讲一个故事来切入正题……
参考
故事从规范的第 8.5.3 节开始;我们需要了解 C++ 现在区分两个不同的引用:
它们的功能在语义上完全相同。然而它们是不同的类型!这意味着以下重载函数也不同:
这将是愚蠢的,如果不是规范中的这句话是正常人永远无法达到的,那么进入第 8.5.3 节:
A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows:
…
If the reference is an rvalue reference, the initializer expression shall not be an lvalue.
查看将引用绑定到左值的简单尝试:
现在可以利用这种特殊行为来实现高级功能。如果你想多玩一点,这里是一个快速开始:
(操作第 33 行以启用非法语句)。
CPP
#include
using namespace std;
// §8.3.2
// References are either form of:
// T& D lvalue reference
// T&& D rvalue reference
// They are distinct types (differentiating overloaded functions)
// §8.5.3
// The initializer of an rvalue reference shall not be an lvalue
// lvalue references
const int& lvr1{ 42 }; // value
int lv1{ 0 };
int& lvr2{ lv1 }; // lvalue (non-const)
constexpr int lv2{ 42 };
const int& lvr3{ lv2 }; // lvalue (const)
constexpr int f1(int x)
{
return 6 * x;
}
const int& lvr4{ f1(7) }; // Function return value
const int& lvr5{ (lv1 + f1(7)) / 2 }; // expression
// rvalue references
const int&& rvr1{ 42 }; // value
// Enable next two statements to reveal compiler error
#if 0
int&& rvr2 {lv1}; // lvalue (non-const)
const int&& rvr3 {lv2}; // lvalue (const)
#else
int&& rvr2{ static_cast(lv1) }; // rvalue (non-const)
const int&& rvr3{ static_cast(lv2) }; // rvalue (const)
#endif
const int&& rvr4{ f1(7) }; // Function return value
const int&& rvr5{ (lv1 + f1(7)) / 2 }; // expression
int main()
{
lv1 = 42;
// Print out the references
cout << lvr1 << " Value" << endl;
cout << lvr2 << " lvalue (non-const)" << endl;
cout << lvr3 << " lvalue (const)" << endl;
cout << lvr4 << " Function return value" << endl;
cout << lvr5 << " Expression (temporary object)" << endl;
cout << rvr1 << " Value" << endl;
cout << rvr2 << " rvalue (const)" << endl;
cout << rvr3 << " rvalue (non-const)" << endl;
cout << rvr4 << " Function return value" << endl;
cout << rvr5 << " Expression (temporary object)" << endl;
return 0;
}
42 Value
42 lvalue (non-const)
42 lvalue (const)
42 Function return value
21 Expression (temporary object)
42 Value
42 rvalue (const)
42 rvalue (non-const)
42 Function return value
21 Expression (temporary object)
移动语义
故事的下一部分需要从规范中的 §12.8 翻译过来。如果对象资源可以被“移动”,它可能比复制对象更快(特别是对于大对象)。这与两种不同的情况相关:
- 初始化(包括参数传递和值返回)。
- 任务。
这些情况依赖于特殊的成员函数来完成工作:
指向 T 的指针在 struct S 的声明中看起来多么无辜!但是,对于大型、复杂的类型 T,对成员_t 的内容的管理可能涉及深拷贝,并且确实会降低性能。每次 struct S 的实例通过函数的参数、一些表达式,然后可能是函数的返回函数:我们花费了更多的时间来复制数据,而不是有效地使用它!
我们可以定义一些替代的特殊函数来处理这个问题。这些函数可以这样编写,而不是复制信息,我们只是从其他对象中窃取它。只是我们不称其为偷窃,它涉及一个更合法的术语:移动它。这些函数利用了不同类型的引用:
请注意,当实际参数是lvalue时,我们将保留原始构造函数和运算符。
但是,如果我们只能强制实际参数为rvalue ,那么我们可以执行这个新的构造函数或赋值运算符!实际上有几种方法可以将左值转换为右值;一种简单的方法是将左值static_cast 为适当的类型:
同样可以以更全面的方式实现,通过指示参数“可用于移动数据”:
最好的洞察力总是在行动中看到它:
CPP
#include
using namespace std;
// §12
// Special member functions
// . §12.1 Constructor
// . §12.8 Copy/Move
// - §12/1 Copy/Move Constructor
// - §13.5.3 Copy/Move Assignment Operator
struct T {
int _v1;
int _v2;
int _v3;
friend std::ostream& operator<<(std::ostream& os, const T& p)
{
return os << "[ " << p._v1 << " | " << p._v2 << " | " << p._v3 << " ]";
}
};
struct S {
S() // Constructor
{
cout << "Constructing instance of S" << endl;
_t = new T{ 1, 2, 3 };
}
S(T& t) // Constructor
{
cout << "Initializing instance of S" << endl;
_t = new T{ t };
}
S(const S& that) // Copy Constructor
{
cout << "Copying instance of S" << endl;
_t = new T;
*_t = *(that._t); // Deep copy
}
S& operator=(const S& that) // Copy Assignment Operator
{
cout << "Assigning instance of S" << endl;
*_t = *(that._t); // Deep copy
return *this;
}
S(S&& that) // Move Constructor
{
cout << "Moving instance of S" << endl;
_t = that._t; // Move resources
that._t = nullptr; // Reset source (protect)
}
S& operator=(S&& that) // Move Assignment Operator
{
cout << "Move-assigning instance of S" << endl;
_t = that._t; // Move resources
that._t = nullptr; // Reset source (protect)
return *this;
}
T* _t;
};
int main()
{
T t1{ 41, 42, 43 };
cout << t1 << " Initializer" << endl;
S s1{ t1 };
cout << s1._t << " : " << *(s1._t) << " Initialized" << endl;
S s2{ s1 };
cout << s2._t << " : " << *(s2._t) << " Copy Constructed" << endl;
S s3;
cout << s3._t << " : " << *(s3._t) << " Default Constructed" << endl;
s3 = s2;
cout << s3._t << " : " << *(s3._t) << " Copy Assigned" << endl;
S s4{ static_cast(s3) };
cout << s4._t << " : " << *(s4._t) << " Move Constructed" << endl;
s2 = std::move(s4);
cout << s2._t << " : " << *(s2._t) << " Move Assigned" << endl;
return 0;
}
[ 41 | 42 | 43 ] Initializer
Initializing instance of S
0x1d13c30 : [ 41 | 42 | 43 ] Initialized
Copying instance of S
0x1d13c50 : [ 41 | 42 | 43 ] Copy Constructed
Constructing instance of S
0x1d13c70 : [ 1 | 2 | 3 ] Default Constructed
Assigning instance of S
0x1d13c70 : [ 41 | 42 | 43 ] Copy Assigned
Moving instance of S
0x1d13c70 : [ 41 | 42 | 43 ] Move Constructed
Move-assigning instance of S
0x1d13c70 : [ 41 | 42 | 43 ] Move Assigned
X值
我们已经到了故事的结尾:
xvalues are also known as eXpiring values.
让我们看看上面例子的移动语义:
S(S &&that) // Move Constructor
{
cout << "Moving instance of S" << endl;
_t = that._t; // Move resources
that._t = nullptr; // Reset source (protect)
}
S& operator=(S &&that) // Move Assignment Operator
{
cout << "Move-assigning instance of S" << endl;
_t = that._t; // Move resources
that._t = nullptr; // Reset source (protect)
return *this;
}
我们通过将资源从参数对象移动到当前对象来实现性能目标。但请注意,在那之后我们也立即使当前对象无效。这是因为我们不想意外地操作实际的参数对象:那里的任何更改都会影响到我们当前的对象,而这并不是我们在面向对象编程中所追求的封装。
该规范给出了表达式为 xvalue 的几种可能性,但让我们记住这一点:
- 对对象的右值引用的强制转换…
概括
Lvalues (Locator values) | Designates an object, a location in memory |
Prvalues (Pure rvalues) | Represents an actual value |
Xvalues (eXpiring values | An object towards the end of its’ lifetime (typically used in move semantics) |