右值引用和移动语义

左值和右值

左值可以取地址、位于等号左边; 而右值没法取地址,位于等号右边。

左右值的概念很清晰,有地址的变量就是左值,没有地址的字面值、临时值就是右值。

注意

  • 一个 rvalue 可以用来初始化一个 rvalue 引用,在这种情况下,rvalue 所标识的临时对象的生命周期会被延长,直到引用的作用域结束。

  • 一个 rvalue绑定到一个 const 左值引用时,编译器会延长该右值的生命周期,使其与引用保持一致。这意味着,即使右值在没有引用的情况下通常会在表达式结束后立即被销毁,但当它被一个 const 左值引用绑定时,它的生命周期会被延长到该引用的生命周期结束为止。

    string Proc()
    {
        return string("abc");
    }
    
    int main()
    {
        const string& ref = Proc();
        cout << ref << endl;
        return 0;
    }
    

左值引用和右值引用

左值引用

就是我们经常使用的引用。能指向左值,不能指向右值

int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败

但是,const左值引用是可以指向右值的:

const int &ref_a = 5;  // 编译通过

const左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用const &作为函数参数的原因之一,如 std::vector 的 push_back : void push_back (const value_type& val);

如果没有 const , vec.push_back(5) 这样的代码就无法编译通过。

右值引用

右值引用的标志是&&,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值。

int &&ref_a_right = 5; // ok
int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
ref_a_right = 6; // 右值引用的用途:可以修改右值

std::move函数

std::move 是一个非常有迷惑性的函数:

  • 不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量;
  • 但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左 值。其实现等同于一个类型转换: static_cast(lvalue) 。 所以,单纯的std::move(xxx) 不会有性能提升。

同样的,右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move 指向该左值:

int &&ref_a = 5;
ref_a = 6; 
等同于以下代码:
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6; 

左值引用、右值引用本身是左值

// 形参是个右值引用
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_right)); // 编译通过
change(std::move(ref_a_left)); // 编译通过

 change(5); // 当然可以直接接右值,编译通过
cout << &a << ' ';
 cout << &ref_a_left << ' ';
 cout << &ref_a_right;
 // 打印这三个左值的地址,都是一样的
}

从上述分析中我们得到如下结论:

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

其实,上述只是对move和右值引用进行语法描述,抛开作用的语法描述是难以理解的

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

浅拷贝重复释放

对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的 重复删除

#include <iostream>
using namespace std;
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 a;
    A b;
    cout << "ready return" << endl;
    if (flag)
        return a;
    else
        return b;
}
int main()
{
    {
        A a = Get(false); // 运行报错
    }
    cout << "main finish" << endl;
    return 0;
}

打印

constructor A

constructor A

ready return

destructor A, m_ptr:0xf87af8

destructor A, m_ptr:0xf87ae8

destructor A, m_ptr:0xf87af8

main finish

注意,上述有两个地址是一致的,因为浅拷贝使用默认的拷贝构造函数,系统会逐个复制非静态数据成员,使用原始对象中每个成员的值来初始化新对象的对应成员。

深拷贝构造函数

正确的做法是提供深拷贝的拷贝构造函数

#include <iostream>
using namespace std;
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 a;
    A b;
    cout << "ready return" << endl;
    if (flag)
        return a;
    else
        return b;
}
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:0xea7af8

destructor A, m_ptr:0xea7ae8

destructor A, m_ptr:0xea7b08

main finish

移动构造函数

这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造 就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对 象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大, 带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?

#include <iostream>
using namespace std;
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 a;
    A b;
    cout << "ready return" << endl;
    if (flag)
        return a;
    else
        return b;
}
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:0xfa7ae8

destructor A, m_ptr:0xfa7af8

main finish

其实,上述代码在运行时编译器依然对其进行了优化,在编译时加上-fno-elide-constructors会让编译器不再优化

运行结果

constructor A
constructor A
ready return
move constructor A 函数返回值临时对象的创建
destructor A, m_ptr:0
destructor A, m_ptr:0x55d6b7adeeb0
move constructor A main函数中对象的创建
destructor A, m_ptr:0
destructor A, m_ptr:0x55d6b7adf2e0
main finish

上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。从移动构造函数的实现 中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,提高了性能。这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷 贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move 语义),右值引用的一个重 要目的是用来支持移动语义的。

移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少 不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护 (创建和销毁)对性能的影响。

调用移动构造函数的优先级比拷贝构造函数的要高,当 rvalue 作为函数参数使用时,如果函数有两个重载,一个重载使用 rvalue 引用参数,另一个重载使用 lvalue 引用常量参数,那么 rvalue 将绑定到 rvalue 引用重载(因此,如果复制和移动构造函数都可用,那么 rvalue 参数将调用移动构造函数,复制和移动赋值操作符也是如此)。

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top