[CPP] 左值 lvalue,右值 rvalue 和移动语义 std::move

参考文章:ios

刷 Leetcode 时,时不时遇到以下 2 种遍历 STL 容器的写法:git

int main()
{
    vector<int> v = {1, 2, 3, 4};
    for (auto &x: v)
        cout<<x<<' ';
    cout<<endl;
    for (auto &&x: v)
        cout<<x<<' ';
    cout<<endl;
}

一个困扰我好久的问题是 auto &auto && 有什么区别?github

左值、右值、纯右值、将亡值

首先要明确一个概念,值 (Value) 和变量 (Variable) 并非同一个东西:ide

  • 值只有 类别(category) 的划分,变量只有 类型(type) 的划分。
  • 值不必定拥有 身份(identity),也不必定拥有变量名(例如 表达式中间结果 i + j + k)。

定义

左值(lvalue, left value),顾名思义就是赋值符号左边的值。准确来讲, 左值是表达式(不必定是赋值表达式)后依然存在的持久对象。函数

右值(rvalue, right value),右边的值,是指表达式结束后就再也不存在的临时对象。性能

C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值和将亡值。优化

纯右值 (prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10, true; 要么是求值结果至关于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、Lambda 表达式都属于纯右值。this

C++( 包括 C ) 中全部的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些能够在多条语句中使用的对象。全部的变量都知足这个定义,在多条代码中均可以使用,都是左值。右值是指临时的对象,它们只在当前的语句中有效。spa

例子:翻译

int i = 0; // ok, i is lvalue, 0 is rval

// 右值也能够出如今赋值表达式的左边, 可是不能做为赋值的对象,由于右值只在当前语句有效,赋值没有意义。
// 0 做为右值出如今了”=”的左边。可是赋值对象是 i 或者 j,都是左值。
(i > 0? i : j) = 233

总结:

  • 全部变量都是左值。
  • 右值都是临时的,表达式结束后不存在,当即数、表达式中间结果都是右值。

特殊状况

须要注意的是,字符串字面量只有在类中才是右值,当其位于普通函数中是左值。例如:

class Foo
{
    const char *&&right = "this is a rvalue"; // 此处字符串字面量为右值
    // const char *&right = "hello world";    // error
public:
    void bar()
    {
        right = "still rvalue"; // 此处字符串字面量为右值
    }
};
int main()
{
    const char *const &left = "this is an lvalue"; // 此处字符串字面量为左值
    // left = "123"; // error
}

将亡值

将亡值 (xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念 (所以在传统 C++ 中,纯右值和右值是同一个概念),也就是即将被销毁、却可以被移动的值。将亡值表达式,即:

  • 返回右值引用的函数的调用表达式
  • 转换为右值引用的转换函数的调用表达式,例如 move

先看一个例子:

vector<int> foo()
{
    vector<int> v = {1,2,3,4,5};
    return v;
}
auto v1 = foo();

按照传统 C++ 的方式(也是咱们这些 C++ 菜鸟的理解),上述代码的执行方式为:foo() 在函数内部建立并返回一个临时对象 v ,而后执行 vector<int> 的拷贝构造函数,完成 v1 的初始化,最后对 foo 内的临时对象进行销毁。

那么,在某一时刻,就存在 2 份相同的 vector 数据。若是这个对象很大,就会形成大量额外的开销。

v1 = foo() 中,v1 是一个左值,能够被继续使用,但foo() 就是一个纯右值, foo() 产生的那个返回值做为一个临时值,一 旦被 v1 复制后,将当即被销毁,没法获取、也不能修改。

而将亡值就定义了这样一种行为: 临时的值可以被识别、同时又可以被移动

在 C++11 以后,编译器为咱们作了一些工做,foo() 内部的左值 v 会被进行隐式右值转换,等价于 static_cast<vector<int> &&>(v),进而此处的 v1 会将 foo 局部返回的值进行移动。也就是后面将会提到的移动语义 std::move()

我的的理解是,这种语法的引入是为了实现与 Java 中相似的对象引用系统。

左值引用与右值引用

区分左值引用与右值引用的例子

先看一段代码:

int a;  
a = 2;  //a是左值,2是右值
a = 3;  //左值能够被更改,编译经过
2 = 3;  //右值不能被更改,错误

int b = 3;  
int* pb = &b;  //pb是左值,&b是右值,由于它是由取址运算符返回的值
&b = 0;  //错误,右值不能被更改

// lvalues:
int i = 42;
i = 43; // ok, i is an lvalue 
int* p = &i; // ok, i is an lvalue 
int& foo();
foo() = 42; // ok, foo() is an lvalue
int* p1 = &foo(); // ok, foo() is an lvalue
// rvalues: 
int foobar(); 
int j = 0;
j = foobar(); // ok, foobar() is an rvalue
int k = j + 2; // ok, j+2 is an rvalue
int* p2 = &foobar(); // error, cannot take the address of an rvalue 
j = 42; // ok, 42 is an rvalue

那么问题来了:函数返回值是否只会是右值?固然不是。

vector<int> v(10, 0);
v[0] = 111;

显然,v[0] 会执行 [] 的符号重载函数 int& operator[](const int x) , 所以函数的返回值也是可能为左值的。

深刻浅出

要拿到一个将亡值,就须要用到右值引用 T &&,其中 T 是类型。右值引用的声明让这个临时值的生命周期得以延长,只要变量还活着,那么将亡值将继续存活。

C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值,有了它咱们就可以方便的得到一个右值临时对象,例如:

#include <iostream>
#include <string>
using namespace std;
void reference(string &str) { cout << "lvalue ref" << endl; }
void reference(string &&str) { cout << "rvalue ref" << endl; }
int main()
{
    string lv1 = "string,"; // lv1 is lvalue
    // string &&r1 = lv1;  // 非法,右值引用不能引用左值
    string &&rv1 = std::move(lv1); // 合法,move 可将左值转移为右值
    cout << rv1 << endl;

    // string &lv2 = lv1 + lv1; // 非法,很是量引用的初始值必须为左值
    const string &lv2 = lv1 + lv1; // 合法,常量左值引用可以延长临时变量的生命周期
    cout << lv2 << endl;

    string &&rv2 = lv1 + lv2; // 合法,右值引用延长临时对象生命周期(经过 rvalue reference 引用 rval)
    rv2 += "Test";
    cout << rv2 << endl;

    reference(rv2); // 输出 "lvalue ref"
    // rv2 虽然引用了一个右值,但因为它是一个引用,因此 rv2 依然是一个左值。
    // 也就是说,T&& Doesn’t Always Mean “Rvalue Reference”, 它既能够绑定左值,也能绑定右值
}

为何不容许很是量引用绑定到左值?

一种解释以下(C++ 真傻逼)。

这个问题至关于解释下面一段代码:

int i = 233;
int &r0 = i; // ok
double &r1 = i; // error
const double &r3 = i; // ok

由于 double &r1 类型与 int i 不匹配,因此不行,那为何 const double &r3 = i 是能够的?由于它实际上至关于:

const double t = (double)i;
const double &r3 = t;

在 C++ 中,全部的临时变量都是 const 类型的,因此没有 const 就不行。

移动语义

先看一段代码,熟悉一下 move 作了些什么:

#include <iostream>
#include <string>
using namespace std;
int main()
{
    string a = "sinkinben";
    string b = move(a);
    cout << "a = \"" << a << "\"" << endl;
    cout << "b = \"" << b << "\"" << endl;
}
// Output
// a = ""
// b = "sinkinben"

而后看完下面一段代码,结束这一回合。

template <class T> swap(T& a, T& b){
  T tmp(a);  //现有两份a的拷贝,tmp和a
  a = b;     //现有两份b的拷贝,a和b
  b = tmp;   //现有两份tmp的拷贝,b和tmp
}

//试试更好的方法,不会生成额外的拷贝
template <class T> swap(T& a, T& b){
  T tmp(std::move(a)); //只有一份拷贝,tmp
  a = std::move(b);    //只有一份拷贝,a
  b = std::move(tmp);  //只有一份拷贝,b
}

我的感受,b = move(a) 这一语义操做,是把变量 b 绑定到数据 a 的内存区域上,从而避免了无心义的数据拷贝操做。

下面这一段代码能够印证个人这个观点。

#include <iostream>
class A
{
public:
    int *pointer;
    A() : pointer(new int(1))
    {
        std::cout << "构造" << pointer << std::endl;
    }
    A(A &a) : pointer(new int(*a.pointer))
    {
        std::cout << "拷贝" << pointer << std::endl;
    } // 无心义的对象拷贝
    A(A &&a) : pointer(a.pointer)
    {
        a.pointer = nullptr;
        std::cout << "移动" << pointer << std::endl;
    }
    ~A()
    {
        std::cout << "析构" << pointer << std::endl;
        delete pointer;
    }
};
// 防止编译器优化
A return_rvalue(bool test)
{
    A a, b;
    if (test)
        return a; // 等价于 static_cast<A&&>(a);
    else
        return b; // 等价于 static_cast<A&&>(b);
}
int main()
{
    A obj = return_rvalue(false);
    std::cout << "obj:" << std::endl;
    std::cout << obj.pointer << std::endl;
    std::cout << *obj.pointer << std::endl;
    return 0;
}
/* Output
构造0x7f8477405800
构造0x7f8477405810
移动0x7f8477405810
析构0x0
析构0x7f8477405800
obj:
0x7f8477405810
1
析构0x7f8477405810
*/

对于 queue 或者 vector,咱们也能够经过 move 提升性能:

// q is a queue
auto x = std::move(q.front());
q.pop();
// v is a vertor
v.push_back(std::move(x));

若是 STL 中的元素「体积」都很大,这么作也能节省一点开销,提升性能。

完美转发

恕我直言,这个翻译是个辣鸡。英文名叫 Perfect Forwarding .

这是为了解决这样一个问题:实参被传入到函数中,当它被再传到另外一个函数中,它依然是一个左值或右值。

template <class T>
void f2(T t){ cout<<"f2"<<endl; }

template <class T>
void f1(T t){ 
    cout<<"f1"<<endl;
    f2(t);  
    //若是t是右值,咱们但愿传入f2也是右值;若是t是左值,咱们但愿传入f2也是左值
}   
//在main函数里:
int a = 2;
f1(3); //传入右值
f1(a); //传入左值

在引进👆巴拉巴拉的这一套机制以前,即 C++11以前的状况是怎么样的呢?当咱们从 f1 调用 f2 的时候,无论传入 f1 的是右值仍是左值,由于 t 是一个变量名,传入 f2 的时候都变成了左值,这就会形成由于调用 T 的拷贝构造函数而生成没必要要的拷贝浪费大量资源。

那么如今有一个叫 forward 的函数,就能够这样作:

template <class T>
void f2(T t){ cout<<"f2"<<endl; }

template <class T>
void f1(T&& t) {    //这是通用引用,而不是右值引用
    cout<"f1"<<endl;
    f2(std::forward<T>(t));  //std::forward<T>(t)用来把t转发为左值或右值,决定于T
}

这样,f1 调用 f2 的时候,调用的就是移动构造函数而不是拷贝构造函数,能够避免没必要要的拷贝,这就叫「完美转发」。

完美转发,傻逼到家。

结语

本文开始提出的问题 auto &auto && 有什么区别?这个问题就更复杂了,涉及到 Universal Reference 这个概念,能够参考这 2 篇文章:

有空再说。

傻逼 C++ 。

相关文章
相关标签/搜索