1.左值 vs 右值

  • 什么是左值?
    • 左值是一个表示数据的表达式
      • 如:变量名、解引用的指针
    • 左值:可以取它的地址 + 对它赋值
  • 什么是右值?
    • 右值是一个表示数据的表达式
      • 如:字面常量、表达式返回值、传值返回函数的返回值
    • 右值:不能取地址
    • 内置类型右值 – 纯右值
    • 自定义类型右值 – 将亡值
int &&rr1 = 10;
double &&rr2 = x + y;
double &&rr3 = fmin(x, y);

2.左值引用 vs 右值引用

  • 无论左值引用还是右值引用,都是给对象取别名
  • 左值引用总结:
    • 左值引用就是给左值的引用,给左值取别名
      • 左值引用只能引用左值,不能引用右值
    • 但是const左值引用既可引用左值,也可引用右值
// x既能接收左值,也能接收右值
template<class T>
void Func(const T& x)
{}
  • 右值引用总结:
    • 右值引用就是对右值的引用,给右值取别名
      • 右值引用只能右值,不能引用左值
    • 但是右值引用可以引用move以后的左值
int&& rr5 = move(b);

3.右值引用使用场景和意义

  • 左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?
  • 左值引用的使用场景:做参数和做返回值都可以提高效率

1.左值引用的短板:

  • 但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回
  • 例如:传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)
    在这里插入图片描述

2.右值引用和移动语义解决上述问题:

  • 在string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了
    • 所以它叫做移动构造,就是窃取别人的资源来构造自己
    • 拷贝构造是深拷贝,移动构造中没有新开空间,拷贝数据,所以效率提高了
// 移动构造
string(string&& s)
    : _str(nullptr), _size(0), _capacity(0)
{
    cout << "string(string&& s) -- 资源转移" << endl;
    swap(s);
}

3.移动构造和拷贝构造的区别

  • 在没有增加移动构造的时候,由于拷贝构造采用的是const左值引用接收参数,因此无论传入的是左值还是右值,都会调用拷贝构造函数
  • 增加移动构造之后,由于移动构造采用的是右值引用接收参数,因此如果拷贝构造对象时传入的是右值,那么就会调用移动构造函数(最匹配原则)
  • string的拷贝构造函数做的是深拷贝,而移动构造函数中只需要调用swap函数进行资源的转移,因此调用移动构造的代价比调用拷贝构造的代价小
  • 注意:
    • 虽然to_string当中返回的局部string对象是一个左值,但由于该string对象在当前函数调用结束后就会立即被销毁,我们把这种即将被销毁的值叫做"将亡值",比如匿名对象也可以叫做"将亡值"
    • 既然"将亡值"马上就要被销毁了,那还不如把它的资源转移给别人用,因此编译器在识别这种"将亡值"时会将其识别为右值,这样就可以匹配到参数类型为右值引用的移动构造函数

4.不仅仅有移动构造,还有移动赋值:

string to_string(int value)
{
    bool flag = true;
    if (value < 0)
    {
        flag = false;
        value = 0 - value;
    }
    string str;
    while (value > 0)
    {
        int x = value % 10;
        value /= 10;
        str += ('0' + x);
    }
    if (flag == false)
    {
        str += '-';
    }
    reverse(str.begin(), str.end());
    return str;
}

// 移动赋值
string& operator=(string&& s)
{
    cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
    swap(s);
    return *this;
}

int main()
{
    string ret1;
    ret1 = to_string(1234);
    return 0;
}

// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义
  • 这里运行后,我们看到调用了一次移动构造和一次移动赋值
  • 因为如果是用一个已经存在的对象接收,编译器就没办法优化了
  • to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后再把这个临时对象做为to_string函数调用的返回值赋值给ret1,这里调用的移动赋值

5.移动赋值和原有operator=()的区别

  • 在没有增加移动赋值之前,由于原有operator=函数采用的是const左值引用接收参数,因此无论赋值时传入的是左值还是右值,都会调用原有的operator=函数
  • 增加移动赋值之后,由于移动赋值采用的是右值引用接收参数,因此如果赋值时传入的是右值,那么就会调用移动赋值函数(最匹配原则)
  • string原有的operator=函数做的是深拷贝,而移动赋值函数中只需要调用swap函数进行资源的转移,因此调用移动赋值的代价比调用原有operator=的代价小

4.右值引用引用左值及其一些更深入的使用场景分析

  • 按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?
    • 因为:有些场景下,可能真的需要用右值去引用左值实现移动语义
    • 当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值
    • C++11中,**std::move()**函数位于头文件中,该函数名字具有迷惑性, 它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
void push_back(value_type&& val);
int main()
{
    list<bit::string> lt;
    bit::string s1("1111");
    // 这里调用的是拷贝构造
    lt.push_back(s1);

    // 下面调用都是移动构造
    lt.push_back("2222");
    lt.push_back(std::move(s1));

    return 0;
}

// 运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义

在这里插入图片描述

5.完美转发

1.模板中的&&万能引用

  • 模板中的&&不代表右值引用,而是万能引用,其既能接受左值又能接收右值

  • 模板的万能引用只是提供了能够同时接受左值引用和右值引用的能力

  • 但是引用类型的唯一作用就是限制了接收的类型后续使用中都退化成了左值

  • 我们希望能够在传递过程中保持的它的左值或者右值的属性,就需要用到完美转发

2.std::forward完美转发在传参的过程中保留对象原生类型属性

// std::forward<T>(t)在传参的过程中保持了t的原生类型属性
template <typename T>
void PerfectForward(T &&t)
{
    Fun(std::forward<T>(t));
}

int main()
{
    PerfectForward(10); // 右值

    int a;
    PerfectForward(a);            // 左值
    PerfectForward(std::move(a)); // 右值

    const int b = 8;
    PerfectForward(b);            // const 左值
    PerfectForward(std::move(b)); // const 右值

    return 0;
}
Logo

2万人民币佣金等你来拿,中德社区发起者X.Lab,联合德国优秀企业对接开发项目,领取项目得佣金!!!

更多推荐