右值引用和移动语义

一、右值引用和移动语义作用

  • C++11 中引入了右值引用和移动语义,可以避免无谓的赋值,提高了程序性能

二、什么是左值、右值

  • int a = 6;
  • 左值可以取地址、位于等号左边。
    • a 可以通过 & 取地址,位于等号左边,所以 a 是左值。
  • 右值没法取地址,位于等号右边。
    • 6 位于等号右边,6 没法通过 & 取地址,所以 6 是个右值。
struct A {
    A(int a = 0) {
        a_ = a;
    }
    int a_;
};

A a = A();
  • a 可以通过 & 取地址,位于等号左边,所以 a 是左值。
  • A() 是个临时值,没法通过 & 取地址,位于等号右边,所以 A() 是个右值。
  • 有地址的变量就是左值,没有地址的字面值、临时值就是右值

三、左值引用

  • 引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝。
  • 左值引用:能指向左值,不能指向右值的就是左值引用
    int a = 5;
    int &ref_a1 = a; // 左值引用指向左值,编译通过
    int &ref_a2 = 5; // 左值引用指向了右值,编译失败
    
  • 引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。
  • 但是,const 左值引用 是可以指向右值的。
    const int &ref = 5; // 编译通过
    
  • const 左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用 const & 作为函数参数的原因之一,如 std::vectorpush_back
    void push_back(const value_type& val);
    
  • 如果没有 constvec.push_back(5) 这样的代码就无法编译通过。

四、右值引用

  • 右值引用的标志是 &&右值引用可以指向右值,不能指向左值
    int &&ref_a_right = 5; 
    
    int a = 5;
    int &&ref_a_left = a; // 编译不通过,右值引用不可以指向左值
    
    ref_a_right = 6; // 右值引用的用途:可以修改右值
    
  • 右值引用指向左值。
    int a = 5; // a 是个左值
    int &ref_a_left = a; // 左值引用指向左值
    int &&ref_a_right = std::move(a); // 通过 std::move 将左值转换为右值,可以被右值引用指向
    cout << a << endl; // 打印结果: 5
    ref_a_left = 10;
    cout << a << endl; // 打印结果: 10
    ref_a_right = 20;
    cout << a << endl; // 打印结果: 20
    
  • std::move
    • 把左值强制转化为右值,让右值引用可以指向左值。
    • 其实现等同于一个类型转换:static_cast<T&&>(lvalue),即单纯的 std::move(xx) 不会有性能提升。
  • 被声明出来的左、右值引用都是左值,因为被声明出的左、右值引用是有地址的,也位于等号左边。
    • std::move 会返回一个右值引用 int&&,它是左值还是右值 ?
      • 从表达式 int&& ref = std::move(a) 来看,右值引用 ref 指向的必须是右值,所以 std::move 返回的 int&& 是个右值。
      • 右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值
// 形参是个右值引用
void change(int&& right_value) {
    right_value = 8;
}
int main() {
    int a = 5; // a 是个左值
    int &ref_a_left = a; // ref_a_left 是个左值引用
    int &&ref_a_right = std::move(a); // ref_a_right 是个右值引用
    
    // change(a); 编译不过,a 是左值,change 参数要求是右值
    // change(ref_a_left); 编译不过,左值引用 ref_a_left 本身也是个左值
    // change(ref_a_right); 编译不过,右值引用 ref_a_right 本身也是个左值
    
    change(std::move(a));
    change(std::move(ref_a_left));
    change(std::move(ref_a_right));
    
    change(5); // 当然可以直接传右值
    
    cout << &a << endl;
    cout << &ref_a_left << endl;
    cout << &ref_a_right << endl;
    return 0;
}

五、结论

  • 从性能上讲,左、右值引用没有区别,传参使用左、右值引用都可以避免拷贝。
  • 右值引用可以直接指向右值,也可以通过 std::move 指向左值,而左值引用只能指向左值(const 左值引用也能指向右值)。
  • 作为函数形参时,右值引用更灵活,虽然 const 左值引用也可以做到左、右值都接受,但它无法修改,有一定局限性。

六、右值引用和 std::move 使用场景

  • 右值引用优化性能,避免深拷贝。
  • 浅拷贝重复释放:对于含有对堆内存的类,我们需要提供深拷贝的拷贝构造函数,否则会导致堆内存的重复删除。
    class A {
    public:
        A(): m_ptr(new int(0)) {
            cout << "constructor A" << endl;
        }
        ~A() {
            cout << "destructor A, m_ptr:" << m_ptr << endl;
            delete m_ptr;
            m_ptr = nullptr;
        }
    private:
        int* m_ptr;
    };
    
    // 为了避免返回值优化
    A Get(bool flag) {
        A a1;
        A a2;
        cout << "ready return" << endl;
        if (flag)
            return a1;
        else
            return a2;
    }
    int main() {
        {
            A a = Get(false); // 赋值,这里调用的是默认的拷贝构造函数,浅拷贝
        }
        cout << "main finish" << endl;
        
        return 0;
    }
    
    //constructor A
    //constructor A
    //ready return
    //destructor A, m_ptr:0xdf14a0
    //destructor A, m_ptr:0xdf1480
    //destructor A, m_ptr:0xdf14a0
    //
    //Process finished with exit code -1073740940 (0xC0000374)
    
  • 深拷贝构造函数:上面代码的默认拷贝构造函数是浅拷贝,main 函数的 aGet 函数的 a2 会指向同一个指针 m_ptr,在析构的时候会导致重复删除该指针,正确的做法是提供深拷贝的拷贝构造函数。
    class A {
    public:
        A(): m_ptr(new int(0)) {
            cout << "constructor A" << endl;
        }
        // 深拷贝
        A(const A& a): m_ptr(new int(*a.m_ptr)) {
            cout << "copy constructor A " << endl;
        }
        ~A() {
            cout << "destructor A, m_ptr: " << m_ptr << endl;
            delete m_ptr;
            m_ptr = nullptr;
        }
    private:
        int* m_ptr;
    };
    
    A Get(bool flag) {
        A a1;
        A a2;
        cout << "ready return" << endl;
        if (flag) {
            return a1;
        } else {
            return a2;
        }
    }
    int main() {
        {
            A a = Get(false); // 深拷贝
        }
        cout << "main finish" << endl;
        return 0;
    }
    
    //constructor A
    //constructor A
    //ready return
    //copy constructor A
    //destructor A, m_ptr: 0xda14a0
    //destructor A, m_ptr: 0xda1480
    //destructor A, m_ptr: 0xda1900
    //main finish
    
  • 移动构造函数:虽然深拷贝构造函数可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 a,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么这个拷贝构造的代价会很大,带来了额外的性能损耗。
  • 下面的代码中没有了拷贝构造,取而代之的是移动构造,从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义,右值引用的一个重要目的是用来支持移动语义的。
  • 移动语义可以将资源(堆,系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。
    class A {
    public:
        A(): m_ptr(new int(0)) {
            cout << "constructor A" << endl;
        }
        // 参数是左值
        A(const A& a): m_ptr(new int(*a.m_ptr)) {
            cout << "copy constructor A" << endl;
        }
        
        // 参数是右值
        // 移动构造函数,可以浅拷贝
        A(A&& a): m_ptr(a.m_ptr) {
            a.m_ptr = nullptr; // 为防止 a 析构时 delete data,提前置空其 m_ptr
            cout << "move constructor A" << endl;
        }
        ~A() {
            cout << "destructor A, m_ptr:" << m_ptr << endl;
            if (m_ptr) {
                delete m_ptr;
            }
        }
    private:
        int* m_ptr;
    };
    
    A Get(bool flag) {
        A a1;
        A a2;
        cout << "ready return" << endl;
        if (flag) {
            return a1;
        } else {
            return a2;
        }
    }
    
    int main() {
        {
            A a = Get(false);
        }
        cout << "main finish" << endl;
        return 0;
    }
    
    //constructor A
    //constructor A
    //ready return
    //move constructor A
    //destructor A, m_ptr:0
    //destructor A, m_ptr:0x901480
    //destructor A, m_ptr:0x9014a0
    //main finish
    
  • 移动语义。
    • move 语义是将对象的状态所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。
    • move 语义起作用,核心在于需要自己实现移动构造函数。
  • forward 完美转发。
    • forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,其传递之后仍然是右值。
      Template<class T>
      void func(T &&val);
      // 这种引用类型既可以对左值引用,也可以对右值引用
      // 但要注意,引用以后,这个 val 值它本质上是一个左值
      int &&a = 10;
      int &&b = a; // 错误
      // a 是一个右值引用,但其本身 a 也有内存名字,所以 a 本身是一个左值
      // 那么再用右值引用引用 a 是错误的
      
    • 因此有了 std::forward() 完美转发,这种 T&& val 中的 val 是左值,但如果我们用 std::forward(val),就会按照参数原来的类型转发。
      int &&a = 10;
      int &&b = std::forward<int>(a);
      

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值