C++11中的右值与移动

一、前言

符号&&的意思是“右值引用”,我们可以给该引用绑定一个右值。“右值”的含义与“左值”正好相反,左值的大致含义是“能出现在赋值运算符左侧的内容”,因此右值大致上就是我们无法为其赋值的值,比如函数调用返回的一个整数。进一步,右值引用的含义就是引用了一个别人无法赋值的内容

二、左值、右值、左值引用、右值引用

  • 左值:一个表示数据的表达式(如:变量名或解引用的指针),且可以通过取地址符(&)获取他的地址,可以对它进行赋值;它可以在赋值符号的左边或者右边。
  • 右值:一个表示数据的表达式(如:字面常量、函数的返回值、表达式的返回值),且不可以通过取地址符(&)获取他的地址;它只能在赋值符号的右边。
  • 左值引用:给左值取别名。左值引用只能引用左值;const左值引用可以左值,也可以引用右值(因为右值通常是不可以改变的值,所以用const左值引用是可以的)。
  • 右值引用:给右值取别名。右值只能引用右值;左值可以通过move(左值)来转化为右值,继而使用右值引用。
      int main()
      {
        /* 1. 以下的a、p、b、c、*p都是左值
        * a、p、b、c 为变量名
        * *p 为解引用的指针
        */
        int* p = new int(0); 
        int b = 1;
        int a = b;
        const int c = 2;
    
        //2. 以下几个都是常见的右值
        10;  //字面常量
        x + y; //表达式的返回值
        fmin(x, y); //函数的返回值
    
        //3. 左值引用只能引用左值,不能引用右值。
        int a = 10;
        int& ra1 = a;   // ra1为a的别名
        //int& ra2 = 10;   // 编译失败,因为10是右值
    
        //4. const左值引用既可引用左值,也可引用右值。
        const int& ra3 = 10;
        const int& ra4 = a;
    
        //5. 右值引用只能右值,不能引用左值。
        int&& r1 = 10;
          
        int a = 10;
        int&& r2 = a;  //error: 无法将左值绑定到右值引用
    
        //6. 右值引用可以引用move以后的左值
        int&& r3 = std::move(a);
    
        return 0;
      }
    

三、移动

    1. 右值引用最常见的用途是实现移动语义(Move Semantics),以避免在对象赋值或作为函数返回值时进行昂贵的深拷贝操作。通过移动语义,可以将资源(如动态分配的内存、文件句柄等)的所有权从源对象“移动”到目标对象,而不是复制它们。例如,在将大型对象(如std::vector、std::string等)赋值给另一个对象时,使用右值引用可以显著提高性能。
    1. 通过std::move标记对象可移动,实现资源高效传递。std::move(x)等价于static_cast<X&&>(x),其中x的类型是X。std::move(x)实际上并不真的移动x(它只是为x创建了一个右值引用),所以其实给它起名rval()的话更贴切,不过std::move()的名字已经使用了太长时间,就一直沿用该名称。
    • 2.1. std::move 只是将对象标记为可移动的;它不会移动任何数据。实际的数据移动发生在被调用移动构造函数或移动赋值运算符时
    • 2.2. 使用std::move 后,原始对象的状态变为有效但未定义(valid but unspecified)。这意味着你不能假设对象在移动后保持其原始值。
    • 2.3. 尽管std::move 允许你传递对象作为右值引用,但它并不保证对象会被移动。如果移动构造函数或移动赋值运算符因为某种原因(如异常安全保证)而选择复制而不是移动,那么对象仍然会被复制。
    1. 移动构造函数实现步骤
    • a. 交换所有权, 通过使用std::move() 等方式将资源从源对象移动到当前对象。
    • b. 设置其他成员变量:除了内部指针之外,移动构造函数还可能需要更新目标对象的其他成员变量,如:std::vector对象,需要更新size_(表示元素数量的成员变量)和 allocator_(如果使用了自定义分配器)
    • c. 将源对象中该资源的状态置为适当的默认状态。例如,在基本类型中将其设为0,在类中可能需要特定的操作。
     template<typename T,typename Allocator = std::allocator<T>>
     class vector {
     private:
       T* data;
       size_t size_;
       size_t capacity_;
       Allocator alloc;
       
       //...其他成员变量和成员函数...
     public:
       //移动构造函数
       vector(vector&& other) noexcept
         :data(other.data),//窃取指针
         size_(other.size_), //窃取大小
         capacity_(other.capacity_), //窃取容量(可选,取决于是否保留容量)
         alloc(std::move(other.alloc))//移动分配器(如果使用了自定义分配器)
       {
         //使源对象变为空(可选,但推荐)
         //注意:这里我们不会释放内存,因为源对象可能还需要它(例如,在返回局部对象的函数中)
         //但我们会确保它不再拥有任何“有效”的元素
         other.data=nullptr;//或指向一个哨兵值,表示空状态
         other.size_= 0;
         other.capacity_=0;//或者保留容量,取决于实现策略
         
         // 注意:通常不需要显式地调用 alloc.deallocate(data),因为源对象可能仍然需要它
         // 真正的内存释放应该在源对象的析构函数中处理(如果那时它仍然拥有它的话)
       }
     }
    
    1. 移动赋值函数实现步骤
    • a. 检查自我赋值:使用 if (this != &other) 确保不是自我赋值。
    • b. 释放自我原有内存
    • c. 交换所有权, 通过使用std::move() 等方式将资源从源对象移动到当前对象。
    • d. 将源对象中该资源的状态置为适当的默认状态。例如,在基本类型中将其设为0,在类中可能需要特定的操作。
    • e. 返回当前对象的引用
    	//类SendData的移动赋值运算符
      SendData& operator=(SendData&& src) noexcept 
      {
        //a.自我检测
        if (this == &src) 
        {
          return *this;
        }
        //b.释放自己的内存
        delete m_pb;
        //c.交换所有权
        m_pb = std::move(src.m_pb);//直接用src内存
        //d.将源对象中该资源的状态置为适当的默认状态
        src.m_pb = nullptr;//斩断原src指向
        //e. 返回当前对象的引用
        return *this;
      }
    

四、如何使用右值引用

  • 右值引用所引对象的使用方式与左值引用所引的对象以及普通变量没什么区别。
  string f(string&& s)
  {
    if (s.size())
      s[0] = toupper(s[0]);
    return s;
  }
  • 调用移动构造函数

    当使用右值构造一个对象时,如果该对象有实现移动构造函数,则调用移动构造函数完成对象构造。

      template<class T> class vector {
        // ...
        vector(const vector& r); //copy constructor (copy r’s representation)
        vector(vector&& r); //move constructor ("steal" representation from r)
      };
      vector<string> s;
      vector<string> s2 {s}; //s is an lvalue, so use copy constructor
      vector<string> s3 {s+"tail"); // s+"tail" is an rvalue so pick move constructor
    
  • 调用移动赋值函数

    当使用右值赋值一个对象时,如果该对象有实现移动赋值函数,则调用移动赋值函数完成对象赋值。

      template<class T>
      void swap(T& a, T& b)
      {
        T tmp {move(a)}; //调用T的移动构造函数构造tmp,实现将a移动到tmp
        a = move(b); //调用T的移动赋值函数对a进行赋值,实现b移动到a
        b = move(tmp); //调用T的移动赋值函数对tmp进行赋值,实现tmp移动b
      }  
    

五、使用移动的注意事项

  • 移动构造函数和移动赋值操作符通常会将被移动对象的资源指针设置为nullptr,以避免资源的重复释放。
  • 移动构造函数和移动赋值操作符通常应该具有noexcept规定,表示它们不会抛出异常。
  • 移动操作并不会自动删除或释放资源,只是转移资源的所有权关系。移动后的对象需要负责管理和释放资源。移动后的源对象状态通常不可预测,应当谨慎使用
  • 使用移动操作时,对象的移后状态应该仍然是有效且可用的。

六、 右值引用的意义

  • 补齐左值引用的短板(避免函数传返回值时对临时对象的拷贝)
  string to_string(int value)
  {
    bool flag = true;
    if (value < 0)
    {
      flag = false;
      value = 0 - value;
    }

    string str;
    //...

    std::reverse(str.begin(), str.end());
    return str;
  }  

  /** 
   * string cpy = to_string(-123456);
   * 1. C++11前,以上代码会经历两次拷贝
   *    a. 局部变量str拷贝给临时对象(将亡值);
   *    b. 临时对象拷贝给局部变量cpy;
   * 但是编译器会自动做返回值优化NRVO(连续的构造,但是不是所有的情况都优化),将两个拷贝构造优化为一个拷贝构造,直接跳过中间的临时变量。
   * 但是对于自定义类型时,虽然将两次拷贝构造优化为一次,拷贝构造仍然要消耗很大的空间,所以这时右值引用的第一个价值就要登场。
   *
   * 2. 使用移动后
   * 编译器将函数的返回值str识别成右值, string cpy = to_string(-123456)中, 直接调用string是移动构造函数完成cpy的构造。
   **/
  • 插入右值数据,减少拷贝

七、const右值引用

不使用const右值引用,因为右值引用的大多数用法都是建立在能够修改所引对象的基础上的。

  • const左值引用和右值引用的比较
    const左值引用和右值引用都能绑定右值,但是它们的目标完全不同:
    • 右值引用实现了一种“破坏性读取”,某些数据本来需要被拷贝,使用右值引用可以优化其性能,减少或者避免拷贝。
    • const左值引用的作用是保护参数内容不被修改。

八、万能引用与完美转发

  • 类型是右值引用的变量,有标识符、有地址,所以类型是右值引用的变量是一个左值
      void foo(const shape&)
      {
        puts("foo(const shape&)");
      }
      void foo(shape&&)
      {
        puts("foo(shape&&)");
      }
      void bar(const shape& s)
      {
        puts("bar(const shape&)");
        foo(s);
      }
      void bar(shape&& s)
      {    
        puts("bar(shape&&)");
        //s是个变量的名字,变量有标识符、有地址,所以它还是一个左值
        foo(s);
      }
      int main()
      {
        bar(circle());
      }
    
      /**1. 以上代码的输出内容为:
      * bar(shape&&)
      * foo(const shape&)
      * 2. 如果我们要让bar调用右值引用的那个 foo 的重载,我们必须写成:
      *    foo(std::move(s)); 或者  foo(static_cast<shape&&>(s));
      * 3. 事实上,很多标准库里的函数,连目标的参数类型都不知道,但我们仍然需要能够保持参数的值类型:左值的仍然是左值,右值的仍然是右值。这个功能在 C++ 标准库中已经提供了,叫std::forward。它和 std::move 一样都是利用引用折叠机制来实现。
        template <typename T>
        void bar(T&& s)
        {
          foo(std::forward<T>(s));
        }
      **
    

1. 引用折叠

引用折叠规则是 C++ 中处理引用的一个重要概念,尤其在模板编程中发挥了关键作用。它决定了当多层引用组合(如引用的引用)时,最终的引用类型。它是C++11中引入的一项特性,主要用于模板编程和完美转发。
引用的引用只能作为别名的结果或者模板类型的参数。

  • 1.1. 引用折叠规则
    组合类型结果类型
    T& &T&
    T& &&T&
    T&& &T&
    T&& &&T&&
  • 1.2. 解释
    • & &:左值引用与左值引用组合,结果仍然是左值引用T&。
    • T& &&:左值引用与右值引用组合,结果仍然是左值引用T&。这是因为在C++中,右值引用绑定到左值时会退化为左值引用
    • T&& &:右值引用与左值引用组合,结果仍然是左值引用T&。这表示如果有一个右值引用被绑定到一个左值引用上,结果会退化为左值引用
    • T&& &&:右值引用与右值引用组合,结果仍然是右值引用T&&。
     /*引用折叠规则*/
     using rr_i = int&&;
     using lr_i = int&;
     using rr_rr_i = rr_i&&; //"int && &&" is an int&&
     using lr_rr_i = rr_i&; //"int && &" is an int&
     using rr_lr_i = lr_i&&; //"int & &&" is an int&
     using lr_lr_i = lr_i&; //"int & &" is an int& 
    
     int&& & r = i; //error: syntax does not allow      
    

2. 万能引用

  • 2.1. 在模板函数中,形参类型写作 T&& 时,可能会出现一种特别的情况:它既能匹配左值引用(lvalue reference),也能匹配右值引用(rvalue reference)。这就是所谓的万能引用。

      template <typename T>
      std::vector<int> inc(T&& v);  
    
  • 2.2. 传入参数时 T 和 T&& 的推导规则

    传入的参数类型T 推导为T&& 实际类型
    左值(a1)std::vector&std::vector& && → 简化为 std::vector&
    右值(a1 + a2)std::vectorstd::vector&&

3. 完美转发

  • 3.1. 在模板编程中,右值引用可以与std::forward结合使用,以实现完美转发(Perfect Forwarding)。这允许函数模板将其参数按照其原始值类别(左值或右值)转发给另一个函数,这在编写泛型代码时特别有用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值