【C++ Primer Chapter 4 总结】左值 & 右值

C++11中的值类型(value categories)
 
基本类型
 
表达式有两个属性:
  1. has identity.  能够确定某个表达式是否和另一个表达式指涉[refers to]同一个实体,例如,通过比较它们标识[identify]出来的函数或者对象的地址(直接或间接得到的)。
  2. can be moved from.  能够被移动构造函数、移动赋值操作符或者其它实现[implement]移动语义[move semantics]的重载函数绑定[bind to]。
 
根据上面两个属性,我们可以对表达式进行分类:
1) 有"身份"但是不能"被移动"的表达式被称为左值表达式[lvalue expression];
2) 有"身份"同时能"被移动"的表达式被称为x值表达式[xvalue expression];
3) 没有"身份"但是能"被移动"的表达式被称为纯右值表达式[prvalue expression];
4) C++没有既没有"身份"也不能"被移动"的表达式;
 
lvalue
左值表达式的属性[properties]:
1) 拥有glvalue[generalized left value, 广义左值]表达式的所有属性。
2) 可以得到一个左值表达式的地址,例如:&++i和&std::endl是合法的表达式。(假设i是内置类型或者重载的前置自增操作符的返回值是左值引用)
3) 可修改的[modifiable]的左值可以被用作内置的赋值或者复合赋值操作符的左操作数[left-hand operand].
4) 左值可以被用来初始化左值引用。这个操作可以将一个确定的对象关联到一个新的名字。
下面的表达式都是左值表达式:
1) 在作用域(scope)内的、无论什么类型的变量名或者函数名。比如,std::cin或者std::endl。即使这个变量的类型是右值引用,由它的名字组成的表达式也是个左值表达式。(右值引用类型的变量组成的表达式也是左值表达式
2) 把左值引用作为返回的函数或者重载操作符。 比如,std::getline(std::cin,str),std::cout << 1, str1 = str2, 或 ++it;
3) a = b, a += b, a %= b, 以及其他所有内置[built-in]的赋值或者复合赋值[compound assignment]表达式。
4) ++a和--a,内置的前置自增[pre-increment]和前置自减[pre-decrement]表达式。
5) *p, 内置的指针取值[indirection]表达式。
6) a[n] 和 p[n], 内置的下标[subscript]索引表达式,【除非[except] ‘a’ 是一个右值数组。【(int[3]) {1,2,3}是右值数组】。(since C++11)】
7) a.m, 对象的取成员变量表达式,除非[except] ‘m’ 是枚举成员或者非静态成员函数,或者 ‘a’ 是右值,以及 ‘m’ 是非引用类型的非静态数据成员。
8) p->m, “对象指针调取内置类型的成员变量” 的表达式,除非[except] ‘m’ 是枚举成员或者非静态成员函数。
9) a.*mp, 对象的成员指针表达式[the pointer to member of object expression],其中,‘a’必须是左值,‘mp’ 是指向数据成员的指针。
10) a->*mp, 对象指针的成员指针表达式,要求:‘mp’指向的是‘a’的数据成员。
11) a,b, 内置的逗号表达式,要求b必须为左值。 
12) a ? b : c, 对于某些a,b,c的三元条件表达式[ternary conditional expression]。
13) 字符串的字面值类型。如: “Hello World”。
14) 强转为左值引用类型的表达式,如:static_cast<int&>(x);
15) 把右值引用作为返回的函数或者重载操作符。【a function call or an overloaded operator expression, whose return type is rvalue reference to function】
16) 强转为函数的右值引用类型的表达式,如:static_cast<void (&&) (int)>(x);
【注:15) 16) 都是从C++11开始使用的】
简单的来说,能取地址的变量一定是左值,有名字的变量也一定是左值,最经典的void fun(p&& shit),其中shit也是左值,因为右值引用是左值(所以才会有move,forward这些函数的产生,其中move出来一定是右值,forward保持变量形式和之前的不变,就是为了解决右值引用是左值的问题)。

 

prvalue【since C++11】rvalue【until C++11】
纯右值表达式的属性[properties]:
1) 拥有右值[rvalue]表达式的所有属性。
2) 纯右值表达式不能是多态的[polymorphic]:纯右值表达式标识对象的动态型别总是表达式的型别。[the dynamic type of the object it identifies is always the type of the expression. ]
3) 非类[non-class]的纯右值不能被const或volatile关键字标识[cv-qualified]。
4) 纯右值不能有不完整的型别[type]。除非[except]它是void类型或者用于decltype说明符[specifier]时。
下面的表达式都是纯右值表达式:
1) 除字符串类型外的字面值[literal],如:42, true, nullptr。
2) 把非引用类型作为返回的函数或者重载操作符。如, str.substr(1,2), str1 + str2, it++。
3) a++ 和 a--,内置的后置自增[post-increment]和后置递减[post-decrement]操作符表达式。
4) a+b, a%b, a&b, a<<b,以及所有其他内置的算术[arithmetic]表达式。
5) a&&b, a||b, ~a,内置的逻辑表达式。
6) a<b, a==b, a>=b,以及所有其他内置的比较表达式。
7) &a, 内置的取址[address-of]表达式。
8) a.m,  对象的取成员变量表达式。其中,‘m’是枚举成员或非静态成员函数,【或者‘a’ 是右值,以及 ‘m’ 是非引用类型的非静态数据成员。(until C++11)】。
9) p->m, 内置的指针取值表达式,其中,m是枚举成员或者非静态成员函数。
10) a.*mp,对象的成员指针表达式[the pointer to member of object expression]。 其中,‘mp’是指向‘a’的成员函数的指针,【或者,‘a’是一个右值,‘mp’是指向数据成员的指针。(until C++11)】。
11) a->*mp, 对象指针的成员指针表达式, 其中, ‘mp’指向的是‘a’的成员函数。 
12) a,b, 内置的逗号表达式,其中,b必须为右值。
13) a?b:c, a ? b : c, 对于某些a,b,c的三元条件表达式[ternary conditional expression]。
14) 强转为非引用类型的表达式,比如,static_cast<double>(x), std::string{}, (int)42 。
15) 【lambda 表达式,比如,[](int x){ return x*x; } (since C++11)】。
纯右值是传统右值的一部分,纯右值是表达式产生的中间值,不能取地址。

 

xvalue【since C++11】
x值表达式的属性[properties]:
1) 拥有右值[rvalue]表达式的所有属性。
2) 拥有泛左值[glvalue]表达式的所有属性。
[注] 类似于纯右值,x值绑定右值引用,但不同的是,x值可能是多态的[polymorphic],并且非类[non-class]的x值可能被const或volatile关键字标识[cv-qualified]。
下面的表达式都是x值表达式:
1) 把右值引用类型作为返回的函数或者重载操作符。例如, std::move(x); 【 a function call or an overloaded operator expression, whose return type is rvalue reference to object
2) a[n], 内置的下标[subscript]表达式,其中,‘a’是一个右值数组。
3) a.m,  对象的取成员变量表达式。其中,‘a’是一个右值,‘m’ 是非引用类型的非静态数据成员。
4)a.*mp, 对象的成员指针表达式[the pointer to member of object expression]。其中,‘a’是右值,‘mp’是指向数据成员的指针。
5) a?b:c, a ? b : c, 对于某些a,b,c的三元条件表达式[ternary conditional expression]。
6) 强转为“对象的右值引用”表达式,比如,static_cast<char&&>(x)。
本质上,消亡值就是通过右值引用产生的值。右值一定会在表达式结束后被销毁,比如return x(x被copy以后会被销毁), 1+2(3这个中间值会被销毁)。
 
混合值类型(maxed gategories)
 
glvalue
广义左值[泛左值, glvalue, generalized left value]表达式是一个左值表达式或者x值表达式。广义表达式“有身份”,它可能能够“被移动”,也可能不能“被移动”。
广义左值表达式的属性[properties],这些属性同样适用于C++11以前的左值:
1) 广义左值可能被隐式地[implicitly]转换为纯右值。这是因为有左值到右值,数组到指针,函数到指针的隐式转换。[a glvalue may be implicitly converted to a prvalue with lvalue-to-rvalue, array-to-pointer, or functon-to-pointer implicit conversion.]
2) 广义左值表达式可能是多态的[polymorphic]:表达式标识对象的动态型别不必是总是表达式的静态型别。
3) 只要表达式合法,广义左值能够有不完整类型。
 
 
rvalue
右值[rvalue, right value]表达式是一个纯右值表达式或x值表达式。右值表达式可能“有身份”,也可能没“有身份”,但能够“被移动”。
右值值表达式的属性[properties],这些属性同样适用于C++11以前的右值:
1) 右值不能[may not]被取地址。例如,&int(),  &i++, &std::move(x)都是不合法的。
2) 右值不能[can not]被用作内置的赋值或复合赋值表达式的左操作数[left-hand operand]。
3) 右值可能被用来初始化常左值引用[const lvalue reference],在这种情况下,这个右值标识对象[the object identified by the rvalue]的生命周期[lifetime]会被延长到这个引用的作用域[scope]的结束。
4) 右值可能被用来初始化常右值引用,在这种情况下,这个右值标识对象[the object identified by the rvalue]的生命周期[lifetime]会被延长到这个引用的作用域[scope]的结束。
5) 作为函数参数[argument],如果有两个重载函数可用[avaliable],其中一个把右值引用作为参数[parameter],另一个把常左值引用作为参数,那么右值会绑定[bind to]那个把右值引用作为参数的重载函数。(因此,在拷贝构造函数和移动构造函数都可用[available]的情况下,右值参数会调用[invoke]移动构造函数,拷贝赋值符号和移动赋值符号与之类似。)
 
 
右值引用 
 
为什么?->怎么实现的?->有什么作用?
 
c++11中增加了右值引用,右值引用关联到右值时,右值被存储到特定位置,右值引用指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置。
右值引用,简单说就是绑定在右值上的引用。语法如下:
int&& r_ref = 10;
 
根据右值引用的语法规则可知,不能将右值引用绑定到一个左值上.
c++11引入右值引用,并且提供了move函数,move()返回右值引用类型。 调用move之后,必须保证除了对l_ref复制或销毁外,我们将不再使用它,在调用move之后,我们不能对移动源后的对象做任何假设。
int& l_ref = r_ref;
int&& r_ref = move(l_ref) 
 
模板实参推断和引用:
当左值引用作为参数时:
template<class T> void f1(T&) {}
f1(i)                       // i是一个int,模板参数类型T是int
f1(ci)                     // ci是一个const int,模板参数T是const int
fl(5)                      // 错误:传递给一个&参数的实参必须是一个左值:左值引用不能绑定到右值上。除非是const左值引用。

  

如果函数的参数是const的引用时:
template<class T> void f2(const T&) {}
f2(i)                          // i是一个int,模板参数类型T是int,因为非const可以转化为const
f2(ci)                         // ci是一个const int,模板参数T是int
f2(5)                         // const的引用可以绑定右值,T是int
 
C++中有两个正常绑定规则的例外,允许将右值引用绑定到一个左值上。这两个例外规则是move正确工作的基础:
例外1:右值引用的类型推断。当我们将一个左值传递给函数的右值引用作为参数时(函数参数为T&&),编译器推断模板类型参数为实参的左值引用类型。因此,调用f3(i)时,T被推断为int&,而不是int。 并且,模板函数中对参数的改变会反映到调用时传入的实参。见 ***处例
通常,我们不能直接定义一个引用的引用,但是同过类型别名(使用typedef)和模板间接定义是可以的。
例外2:引用折叠。当定义了引用的引用时,则这些引用形成了“折叠”,所有的情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。这个例外就是右值引用的右值引用:
  • X&  &&、X&  &&、X&&  &都折叠成 X&
  • 类型X&&  &&折叠成X&&
 
当参数是右值引用时:
template<class T> void f3(T&&) {}
f3(i)                             //根据上述例外1,实参是左值int,模板参数T是int&
f3(ci)                            //根据上述例外1,实参是左值const int,模板参数T是一个const int&
f3(5)                             // 实参是右值,T是int

 

当T被推断为int&时,函数f3会实例化成如下的样子:
void f3<int&> (int&  &&)
void f3<int&> (int&)
 
这两个规则导致了两个重要的结果:
  • 如果一个函数参数是一个指向模板类型参数的右值引用,如T&&,则它能被绑定到一个左值,且
  • 如果实参是一个左值,则推断出的模板实参类型将时一个左值引用,且函数参数被实例化为一个普通左值引用参数(T&)
值得注意,参数为T&&类型的函数可以接受所有类型的参数,左值右值均可。在前面,同样介绍过,const的左值引用做参数的函数同样也可以接受所有类型的参数。
当重载函数同时出现时,右值引用做参数的函数绑定非const右值,const左值引用做参数的函数绑定左值和const右值(非const右值就是通过右值引用来引用的右值,虽然无法获取右值的地址,但是可以通过定义右值引用来更改右值):
Template<class T> void f(T&&)                           //绑定到非const右值
Template<class T> void f(const T&)                    //左值和const右值

***
通过前面,我们了解到当右值引用作为函数模板参数时且实参是左值,类型T会被推断为一个引用类型。这一特性会影响模板函数内部的代码,看下面一段代码:
template<class T>
void f3(T&& val)  
{
   T t = val;         //int& t = val; t的改变会反映到val上
   t = fcn(t);
   if(val == t){…}      //一直为true
}
f3(i);             // i是左值,T是int& 
***
 
完美转发
某些函数需要将其中一个或多个实参连同类型不变地转发给其他函数,在这种情况下,我们需要保持被转发实参的所有性质,包括实参是否是const的、以及是左值还是右值。
  • 只有在需要的时候,才调用复制构造函数
  • 左值被转发为左值,右值被转发为右值
void foo(myVector& v) {}
void foo(myVector&& v) {}
 
// 参数转发
template<typename T>void relay(T&& arg) {
    foo(std::forward<T>(arg));
}
 
// 假设有一个函数,返回值是一个 MyVector
myVector createMyVector();
 
int main() {
  myVector reusable= createMyVector();
  
  // 拷贝构造函数
  relay(reusable); // 调用 foo(myVector&),reusable是左值
 
  // 移动构造函数
  relay(createMyVector()); // 调用 foo(myVector&&),因为createMyVector()得到的是个临时的右值,如果relay()函数中没用用forward()转发,则调用的是foo(myVector&),因为arg虽然是右值引用类型,其是个左值(有变量名)。
 
}

  

在旧的c++中,出现了很多的不必须要的拷贝,因为在某些情况下,对象拷贝完之后就销毁了。新标准引入了移动操作,减少了很多的复制操作,而右值引用正是为了支持移动操作而引入的新的引用类型。
右值引用的作用:移动语义和完美转发。
std::vector<int> v1{1, 2, 3, 4, 5};
std::vector<int> v2;
std::vector<int> v3;
 
 
v2 = v1;                         // 拷贝赋值操作符--复制语义
std::cout << v1.size() << std::endl;         // 输出 5
std::cout << v2.size() << std::endl;         // 输出 5
v3 = std::move(v1);                   // 移动赋值操作符--移动语义
std::cout << v1.size() << std::endl;        // 输出 0
std::cout << v3.size() << std::endl;        // 输出 5

  

为了实现移动语义,C++ 增加了与拷贝构造函数(copy constructor)和拷贝赋值操作符(copy assignment operator)对应的移动构造函数(move constructor)和移动赋值操作符(move assignment operator),通过函数重载机制来确定应该调用拷贝语义还是移动语义(参数是左值引用就调用拷贝语义;参数是右值引用就调用移动语义)。
右值的内容可以直接移动(move)给左值对象(移动语义),而不需要进行开销较大的深拷贝(deep copy)。
 
为什么需要移动语义?
class myVector {
  int size;
  double* array;public:
 
  myVector(const myVector& rhs) {                // 复制构造函数
    std::cout << "Copy Construct\n";
    size = rhs.size;
    array = new double[size];
    for (int i=0; i<size; i++) {
      array[i] = rhs.array[i];
    }
  }
 
 
    myVector(int n) {
        size = n;
        array = new double[n];
    }};void foo(myVector v) {
  /* Do something */
  }
 
 

myVector createMyVector();                     //假设有一个函数,返回值是一个 MyVector
 
 
int main() {
  // Case 1:
  myVector reusable=createMyVector();
  foo(reusable);                   // 这里会调用 myVector 的复制构造函数,如果我们不希望 foo 随意修改 reusable,这样做是 ok 的
  /* Do something with reusable */  
 
  // Case 2:
  foo(createMyVector());             // createMyVector() 会返回一个临时的右值,传参过程中定义形参变量会调用拷贝构造函数,该右值被复制给形参(这是多余的被复制一次),然后该右值就会被销毁,如果可以直接用该右值就避免一次深拷贝
  foo(std::move(reusable));           // 在 C++03 中,为了解决这个问题,可能需要定义两个 foo 函数,比较麻烦: foo_by_value(myVector) 和  foo_by_value(myVector). C++11之后可以使用移动语义了,将调用移动构造函数。
}  

  

 
 
示例:
移动语义:
#include <iostream>#include <string>#include <vector>class Foo {
public:
  // 默认构造函数
  Foo() { std::cout << "Default Constructor: " << Info() << std::endl; 
  }
 
  // 自定义构造函数
  Foo(const std::string& s, const std::vector<int>& v) : s_(s), v_(v) {
    std::cout << "User-Defined Constructor: " << Info() << std::endl;
  }
 
  // 析构函数
  ~Foo() { std::cout << "Destructor: " << Info() << std::endl; }
 
 
  // 拷贝构造函数
  Foo(const Foo& f) : s_(f.s_), v_(f.v_) {
    std::cout << "Copy Constructor: " << Info() << std::endl;
  }
 
  // 拷贝赋值操作符
  Foo& operator=(const Foo& f) {
    s_ = f.s_;
    v_ = f.v_;
    std::cout << "Copy Assignment: " << Info() << std::endl;
    return *this;
  }

 
  // 移动构造函数
  Foo(Foo&& f) : s_(std::move(f.s_)), v_(std::move(f.v_)) {
    std::cout << "Move Constructor: " << Info() << std::endl;
  }
 
  // 移动赋值操作符
  Foo& operator=(Foo&& f) {
    s_ = std::move(f.s_);
    v_ = std::move(f.v_);
    std::cout << "Move Assignment: " << Info() << std::endl;
    return *this;
  }
 
 
  std::string Info() {
    return "{" + (s_.empty() ? "'empty'" : s_) + ", " +
           std::to_string(v_.size()) + "}";
  }
 
private:
  std::string s_;
  std::vector<int> v_;
};

int main() {
  std::vector<int> v(1024);
 
  std::cout << "================ Copy =======================" << std::endl;
  Foo cf1("hello", v);
  Foo cf2(cf1);  // 调用拷贝构造函数
  Foo cf3;
  cf3 = cf2;  // 调用拷贝赋值操作符
  
  std::cout << "================ Move =========================" << std::endl;
  Foo f1("hello", v);
  Foo f2(std::move(f1));  // 调用移动构造函数
  Foo f3;
  f3 = std::move(f2);  // 调用移动赋值操作符
  return 0;
}

  

在这个例子中,每次都会拷贝 s_ 和 v_ 两个成员,最后 cf1、cf2、cf3 三个对象的内容都是一样的。 每次执行移动语意,是分别调用 s_ 和 v_ 的移动语意函数——理论上只需要对内部指针进行修改,所以效率较高。执行移动语意的代码片段了出现了一个标准库中的函数 std::move —— 它可以将参数强制转换成一个右值。本质上是告诉编译器,我想要 move 这个参数——最终能不能 move 是另一回事——可能对应的类型没有实现移动语意,可能参数是 const 的。 有一些场景可能拿到的值直接就是右值,不需要通过 std::move 强制转换。
 
完美转发:
void Process(const Foo& f) {
  std::cout << "lvalue reference" << std::endl;
  // ...
}
 
 
// 接受一个右值引用
void Process(Foo&& f) {
  std::cout << "rvalue reference" << std::endl;
  // ...
}
 
 
template <typename T>void LogAndProcessNotForward(T&& a) {
  std::cout << a.Info() << std::endl;
  Process(a);
}
 
 
template <typename T>void LogAndProcessWithForward(T&& a) {
  std::cout << a.Info() << std::endl;
  Process(std::forward<T>(a));}
 
 
LogAndProcessNotForward(f3);                                // 输出 lvalue reference
LogAndProcessNotForward(std::move(f3));                   // 输出 lvalue reference
LogAndProcessWithForward(f3);                             // 输出 lvalue reference
LogAndProcessWithForward(std::move(f3));                  // 输出 rvalue reference
 
LogAndProcessNotForward(f3); 和 LogAndProcessWithForward(f3); 都输出 "lvalue reference",这一点都不意外,因为 f3 本来就是一个左值。
LogAndProcessNotForward(std::move(f3)); 输出 "lvalue reference" 是因为虽然参数 a 绑定到一个右值,但是参数 a 本身是一个左值。
LogAndProcessWithForward(std::move(f3)); 使用了 std::forward 对参数进行转发,std::forward 的作用就是:当参数是绑定到一个右值时,就将参数转换成一个右值。

  

posted @ 2021-05-29 16:37  萌新的学习之路  阅读(241)  评论(0编辑  收藏  举报