> 文档中心 > c++左值、右值引用和移动语义

c++左值、右值引用和移动语义

c++左值、右值引用和移动语义

  • 一、左值和右值
  • 二、左值引用&
  • 三、右值引用&&
    • 应用:右值引用优化性能,避免深拷贝
  • 四、移动语义 move
  • 五、完美转发forward

参考链接

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

一、左值和右值

左值是表达式结束后仍然存在的持久对象,右值是指表达式结束时就不存在的临时对象。
区分表达式的左右值属性:如果可对表达式用&符取址,则为左值,否则为右值

比如:

int a=10;int b=a;

对于a=10,由于10是临时的,表达式结束这个值就不存在了,因此是右值。而a=10中,a作为接受的变量,是表达式结束后仍然存在的对象,因此是左值。
对于b=a,由于a、b都是持久存在的,因此都是左值。

二、左值引用&

&只能用作左值引用,因此对于下面的test(int& x),中的参数x一定是左值
对于一个常数10,它是右值,因此没法调用test(10);

void test(int& x){//这个x一定是左值    cout<<x<<endl;}int main(){    int a=10;    //test(10); //传入的是右值,因此会报错    test(a); //传入的是左值}

三、右值引用&&

右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内
存,只是该对象的一个别名。

通过右值引用的声明,该右值又“重获新生”,其生命周期其生命周期与右值引用类型变量的生命周期一
样,只要该变量还活着,该右值临时量将会一直存活下去。

如下面代码,10作为右值,正常情况下,在使用完后该临时变量就应该消失了。但是int&& x相当于给这个10取了别名为x,也叫右值引用。它的好处是避免了拷贝。

void test(int&& x){    cout<<x<<endl;}int main(){    int a=10;    test(10); }

&&总结:

  • 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值(也就是说对于&,只能用作左值引用,而&&既可以作为左值引用也可以作为右值引用,取决于传入的参数是左值还是右值)
  • auto&& 或函数参数类型自动推导的 T&& 是一个未定的引用类型,被称为 universal references,
    它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。
  • 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引 用。当 T&& 为
    模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右 值引用
  • 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值

应用:右值引用优化性能,避免深拷贝

A(const A& a)是拷贝构造函数,如果没有移动构造函数A(A&& a)的情况下,执行 A a = Get(false),调用的是拷贝构造函数,它会将Get(flase)返回的结果拷贝给a,然后执行析构函数,但是这样会造成性能的损耗。
因此可以使用移动构造函数A(A&& a),由于Get(false)得到的是一个右值,因此会调用移动构造函数,只需要将需要构造的对象的指针拷贝就行了,将被移动的对象的指针置为空,就完成了移动拷贝构造。(避免了深拷贝)

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

#include 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; 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;}

四、移动语义 move

我们知道移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借组移动语义来优化性
能呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语
义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝

int main(){MyString a;a = MyString("Hello"); // moveMyString b = a; // copyMyString c = std::move(a); // move, 将左值转为右值return 0;}

五、完美转发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);

例子:

#include using namespace std;template <class T>void Print(T &t){    cout << "L" << t << endl;}template <class T>void Print(T &&t){    cout << "R" << t << endl;}template <class T>void func(T &&t) // 左值右值都只能走这个{    cout << "func(T &&t)" << endl;    Print(t);  // L   (什么都不做,默认当作左值)    Print(std::move(t));// 肯定调用"R"    Print(std::forward<T>(t)); // 不确定L  R  ,如果要完美往下层函数转发加forward}int main(){    cout << "-- func(1)" << endl;    func(1);  //  1是R    int x = 10;    int y = 20;    cout << "-- func(x)" << endl;    func(x);  // x本身是左值     (因此后续调用Print(std::forward(t))的时候,传入的依然是左值)    cout << "-- func(std::forward(y))" << endl;    func(std::forward<int>(y)); // std::forward(y)变成右值  (如果传入的参数,本身不带有&&符号,那么forward就把它当作右值)    return 0;}

可以看到结果,分别是3个fun调用的输出。
c++左值、右值引用和移动语义
对于func(1)1是右值。func(T &&t)因此这边t的语言应该是右值,但是由于编译器会将已命名的右值看作左值,因此Print(t)中传入的是左值,Print(std::move(t))这里肯定是调用右值,因为显示地转为右值了。Print(std::forward(t)),传入的值是根据&&类型的值,原来的类型决定,因为传入&&前,是右值,因此Print(std::forward(t))在forward调用后是右值。

其他3种func情况也是类似的理解。
另外对于 func(std::forward(y));由于y本身并不带有&&符号,因此forward后,默认是变成右值的。(什么是带&&符号?就是func(T &&t)这个函数的参数带了&&)