通过示例了解C/C++中的Lvalue,PRvalue和Xvalue

2021年4月24日13:21:04 发表评论 1,404 次浏览

C中的LValue和RValue

背景

即将阅读本文的人当中, 可能有很多人希望澄清一下以前的基本知识:右值是可以在赋值运算符右侧弹出的东西, 并且左值是属于赋值运算符左侧或右侧的事物。毕竟, 这是k&R区分某些表达式与其他表达式的方式:

对象是存储的命名区域。左值是引用对象的表达式。左值表达式的一个明显示例是具有适当类型和存储类的标识符。有些运算符会产生左值:例如, 如果E是指针类型的表达式, 那么* E是一个左值表达式, 它引用E指向的对象。名称"左值"来自赋值表达式E1 = E2, 其中左操作数E 1必须是左值表达式。每个运算符的讨论都指定了是否期望左值操作数以及是否产生左值。

不幸的是, 这种简单的方法现在已成为黑暗时代的顽固纪念品。当我们有足够的勇气查阅最新的规范时, 第3.10段将以下分类法摆在了我们的面前:

expression
          /     \
    glvalue       rvalue
       /    \      /    \
lvalue         xvalue        prvalue

google人类可读的澄清了超过规范本身,搜索结果进入区分左值和右值引用的引用,将语义的细微差别,实际上……所有的这些先进的特性,需要这个令人困惑的层次结构的基本概念。

好吧, 本文提供了截然不同的内容:对于那些初看这些术语的人们, 它会尝试从所有这些中获得一些意义, 而无需提高情绪的方法来理解所有这些……我们甚至可以提供第一个建议我们需要谨记:

忘掉赋值运算符左右两侧的赋值和杂物。

这棵语义标签树中最大的挑战是神秘值。我们不必了解值, 这是给势利者的。我们可以限制自己去理解左值和前值。如果你已经了解值, 你可以快速擦亮你的"精英C++++程序员"牌匾, 并寻找有关将这些冠冕堂皇的文章值很好地利用。对于我们其他人, 我们可以将当前段落改写为第二条建议:

专注于理解各种表达式中的左值和右值。

左值

我们正在谈论表达式的语法和语义, 并且将赋值巧妙地埋入了此类表达式的BNF(Backus-Naur-Form)中。这就是第二条建议建议忘记作业的原因。因为规范仍然很清楚左值是!但是, 我们不提供冗长的描述, 而只是提供一些源代码示例:

//Designates an object
int lv1;         

//Reference, designates an object       
int &lv2        {lv1}

//Pointer, designates an object   
int *lv3;               

//Function returning a reference, designates an object
int &lv4() {            
  return lv1;
}

就是这样(或多或少)!好了, 我们可以弄清楚类是如何类型的, 类实例也是对象, 然后从那里观察实例和成员的引用和指针也是对象。但是, 这恰恰是这种解释类型, 它使我们不知所措, 从而使细节难以理解!在这一点上, 我们有一个典型示例, 说明了四种不同的左值。规范并没有规定任何限制, 仅属于赋值运算符的左侧或右侧!一个左值是最终将对象定位在内存中的表达式。

因此, 将左值更恰当地描述为"定位符值"。

在这一点上,我们必须承认我们在初始化表达式中混入了一个左值:lv1不仅仅是声明它的语句中的左值!即使使用lv1来初始化lv2引用(引用必须初始化,始终如此),lv1仍然是左值!

说明左值用法的最好方法是将其用作结果存储的定位符以及数据输入的定位符。继续观察它们的实际作用:

//CPP program to illustrate the concept of lvalue
#include <iostream>
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 assigment 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;
}

输出如下:

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

Prvalue

我们现在跳过更复杂的右值。在前面提到的黑暗时代,它们微不足道。现在它们包括了听起来神秘的xvalues!我们想要忽略这些xvalues,这正是prvalue的定义让我们做的:

prvalue是一个不是xvalue的右值。

或者稍微简单一点:

prvalue代表直接值。

这在初始化程序中最明显:

int prv1                {42};   //Value

但是, 另一种选择是使用左值初始化:

constexpr int lv1       {42};
int prv2                {lv1};  //Lvalue

这里发生了什么!这本来应该很简单, 左值成为前值???在规范中, 第3.3.1节中有一个句子可以挽救:

每当在期望有prvalue的上下文中出现glvalue时, 该glvalue就会转换为prvalue。

让我们忽略一个事实,glvalue只是一个左值或x值。我们已经从这个解释中禁止了xvalues。因此:我们如何从rv2的左值中获得一个值(prvalue) ?通过转换(计算)它!

我们可以使它变得更加有趣:

constexpr int f1(int x} {
  return 6*x;
}
int prv3  {f1(7)};  //Function return value

我们现在有一个功能f1(), 返回一个值。规范的确提供了临时变量(左值), 然后将其转换为前值在需要的地方。只是假装这正在发生:

int prv3 {t}; //Temporary variable t created by compiler
                   //. not declared by user), //- initialized to value returned 
                   //by f1(7)

对于更复杂的表达式也有类似的解释:

int prv4 {(lv1+f1(7))/2};//Expression: temporary variable
                                    //gets value of (lv1+f1(7))/2

小心点!右值不是对象, 也不是函数。右值最终使用的是:

  • 文字的值(与任何对象无关)。
  • 函数返回的值(与任何对象无关, 除非我们计算用于返回值的临时对象)。
  • 保留表达式求值结果所需的临时对象的值。

对于通过执行编译器学习的人:

//CPP program to illustrate glvalue
#include <iostream>
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

Xvalue

等等:我们不是要讨论x值吗?!在这一点上,我们已经知道了左值和内值实际上并没有那么难。几乎是任何一个理智的人都会想到的。我们不希望在阅读所有这些文本时感到失望,只是为了确认左值涉及一个可定位对象,而prevalues指向某个实际值。因此有了这个惊喜:我们也可以讨论xvalues,然后我们就完成了,并理解了所有的xvalues !

不过, 我们需要讲一些故事来说明重点……

参考文献

故事从规范的第8.5.3节开始;我们需要了解, C++++现在可以区分两个不同的

引用:

int&  //lvalue reference
int&&  //rvalue reference

它们的功能在语义上完全相同。但是它们是不同的类型!这意味着以下重载函数也有所不同:

int f(int&);
int f(int&&);

如果不是规范中的这一句话没有正常的人类无法做到的话, 这真是愚蠢的, 请参见§8.5.3:

类型" cv1 T1"的引用由类型" cv2 T2"的表达式初始化, 如下所示:…如果该引用是右值引用, 则初始化程序表达式不得是左值。

看一个简单的尝试将引用绑定到左值:

int lv1         {42};
int& lvr        {lv1};    //Allowed
int&& rvr1      {lv1};   //Illegal
int&& rvr2      {static_cast<int&&>(lv1)};//Allowed

现在可以利用此特定行为获取高级功能。如果你想多玩一点, 这里是一个快速入门:

(操纵第33行以启用非法声明)。

#include <iostream>
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 <int &&>(lv1) }; //rvalue (non-const)
const int && rvr3{ static_cast <const int &&>(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中进行翻译。如果可以"移动"对象资源, 可能比复制对象要快(尤其是对于大型对象)。这在两种不同情况下相关:

  1. 初始化(包括参数传递和值返回)。
  2. 分配。

这些情况依靠特殊的成员函数来完成工作:

struct S {
  S(T t) : _t(t) {}  //Constructor
  S(const S &s); //Copy Constructor
  S& operator=(const S &s); //Copy Assignment Operator
  T* _t;
};

T t1;
S s1    {t1};    //Constructor with initialization
S s2    {s1};    //Constructor with copy
S s3;        //Constructor with defaults
s3 = s2;    //Copy assignment operator

指向T的指针在结构S的声明中看起来多么纯真!但是, 对于大型, 复杂的类型T, 成员_t内容的管理可能涉及深拷贝, 并确实降低了性能。每当struct S实例遍历一个函数的参数, 一些表达式, 然后可能返回一个函数的返回值时:我们花费更多的时间来复制数据, 而不是有效地使用它!

我们可以定义一些替代的特殊功能来处理此问题。这些函数的编写方式是, 我们无需复制信息, 而只是从其他对象中窃取信息。只有我们不称其为偷窃, 它涉及更多法律术语:移动它。这些函数利用了不同类型的引用:

S(const S &&s); //Move Constructor
  S& operator=( S &&s); //Move Assignment Operator

注意,当实际形参是左值时,我们保留了原始构造函数和操作符。

但是,如果只能强制实际形参为右值,那么就可以执行这个新的构造函数或赋值操作符!实际上有几种方法可以将左值转换为右值;一种简单的方法是用static_cast将左值转换为适当的类型:

S s4 {static_cast<S&&>(s3)); //Calls move constructor
s2 = static_cast<S&&>(s4); //Calls move assignment operator

通过指示参数"可用于移动数据", 可以通过更全面的方式实现这一点:

S s4 {std::move(s3)); //Calls move constructor
S2 = std::move(s4); //Calls move assignment operator

最好的见解总是能看到它的实际效果:

#include <iostream>
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 <S&&>(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

Xvalues

我们到了故事的结尾:

xvalues也称为扩展值。

让我们看一下上面示例的移动语义:

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提供了几种可能性, 但让我们记住这一点:

强制转换为对对象的右值引用…

左值(定位符值) 指定一个对象, 在内存中的位置
Prvalues(纯rvalues) 代表实际值
Xvalues(扩展值) 对象生命周期即将结束(通常在移动语义中使用)

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: