📜  通过示例了解C / C++中的左值,PR值和X值

📅  最后修改于: 2021-05-26 00:14:30             🧑  作者: Mango

C中的LValue和RValue

背景

想要阅读本文的人当中,有很多人可能想澄清一下以前是基本的东西: rvalues是可以在赋值运算符右侧弹出的东西,而lvalues是属于左侧或左侧的东西。对一个赋值运算符的权利。毕竟,这是k&R区分某些表达式与其他表达式的方式:

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

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

搜寻比规范本身更多的人类可读性说明,搜索结果进入左值引用右值引用之间的差异,移动语义的细微差别,……事实上,所有这些高级功能都需要在基础概念中使用这种令人困惑的基本概念层次结构第一名。
好吧,本文提供了截然不同的内容:对于那些初次了解这些术语的人们,它会尝试从所有这些中获得一些意义,而无需提高情绪的方法来理解所有这些……我们甚至可以提供第一个建议我们需要谨记:

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

这种语义标签树中最大的挑战是神秘的xvalue 。我们不必了解xvalues ,这是针对势利小人的。我们可以限制自己的理解左值prvalues。如果您已经了解xvalues ,则可以快速完善您的“精英C++程序员”牌匾,并寻找有关如何充分利用这些xvalues的其他文章。对于我们其他人,我们可以将当前段落改写为第二条建议:

着重于理解各种表达式中的左值和右值。

左值

我们正在谈论表达式的语法和语义,并且将赋值巧妙地埋入了此类表达式的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;
}

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

在这一点上,我们必须承认,我们在一个悄悄在初始化表达式:LV1不只是在声明中的左值在声明!即使使用lv1初始化lv2引用(始终必须对引用进行初始化), lv1仍然是左值
说明左值用法的最好方法是将其用作结果存储的定位符以及数据输入的定位符。继续观察它们的实际作用:

// 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 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

实用价值

现在,我们正在跳过更复杂的右值。在前面提到的黑暗时代,它们是微不足道的。现在,它们包括了神秘的听起来很有价值的xvalues !我们想忽略那些xvalues ,这正是prvalue的定义让我们能够做到的:
prvalue是不是xvalue右值
或减少一些混淆:

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

int prv1                {42};   // Value

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

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

这里发生了什么!这本来很简单,一个左值怎么可能是一个pr?在规范中,第3.3.1节中有一个句子可以挽救:

让我们忽略一个事实,即glvalue就是lvaluexvalue 。我们已经禁止在此解释中使用xvalues了。因此:我们怎样才能从左值RV2(prvalue)?通过转换(评估)它!
我们可以使它变得更加有趣:

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

现在,我们有一个函数f1() ,它返回一个值。规范确实提供了引入临时变量( lvalue )的情况,然后在需要时将其转换为prvalue 。只是假装这正在发生:

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 
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

X值

等待:我们不打算谈论xvalues吗?!那么,在这一点上,我们已经了解到,左值prvalues是真的没有那么难毕竟。几乎任何理性的人都会期望的。我们不希望通过阅读这一切的文字很失望,只是要确认左值涉及的可定位对象,prvalues参考一些实际值。因此,这令人惊讶:我们可能还涵盖了xvalues ,那么我们就完成了并理解了所有xvalues
不过,我们需要讲一些故事来说明重点……

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

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

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

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

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

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

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

现在可以利用此特殊行为来获得高级功能。如果您想多玩一点,这里是一个快速入门:
(操纵第33行以启用非法声明)。

#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中进行翻译。如果可以“移动”对象资源,则可能比复制对象要快(特别是对于大型对象)。这在两种不同情况下是相关的:

  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

请注意,当实际参数为左值时,我们保留原始的构造函数和运算符。
但是,如果仅可以将实际参数强制为rvalue ,则可以执行此新的构造函数或赋值运算符!实际上,有几种方法可以将左值变成右值;一种简单的方法是将左值static_cast转换为适当的类型:

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

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

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

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

#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值

我们到了故事的结尾:

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

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)