c++11右值引用

来源:互联网 发布:python怎么输入 编辑:程序博客网 时间:2024/06/06 01:49

c++11中引入了右值引用和移动语义,可以避免无谓的复制,提高了程序的性能。
我们平常所说的引用通常是指左值引用,用&表示。而右值引用,用&&表示。
要介绍右值引用的作用以及如何使用之前,我们必须要搞明白什么是左值,什么是右值。


左值与右值

左值:指表达式结束后依然存在的持久对象。
右值:指表达式结束时就不再存在的临时对象。

一个区分左值与右值的便捷方法:

看看能不能对表达式取地址,如果能,则为左值,否则为右值。
所有的具名变量或对象都是左值,而右值不具名。

举例:

 int a = 10; int b = 20; int *pFlag = &a; vector<int> vctTemp; vctTemp.push_back(100); string str1 = string("hello"); string str2 = "world"; const int &m = 1;

请问,a,b, a+b, a++, ++a, pFlag, *pFlag, vctTemp[0], 100, string(“hello”), str1, str1+str2, m分别是左值还是右值?

a和b都是持久对象(可以对其取地址),是左值;
a+b是临时对象(不可以对其取地址),是右值;
a++是先取出持久对象a的一份拷贝,再使持久对象a的值加1,最后返回那份拷贝,而那份拷贝是临时对象(不可以对其取地址),故其是右值
++a则是使持久对象a的值加1,并返回那个持久对象a本身(可以对其取地址),故其是左值
pFlag和*pFlag都是持久对象(可以对其取地址),是左值;
vctTemp[0]调用了重载的[]操作符,而[]操作符返回的是一个int &,为持久对象(可以对其取地址),是左值;
100和string(“hello”)是临时对象(不可以对其取地址),是右值;
str1是持久对象(可以对其取地址),是左值;
str1+str2是调用了+操作符,而+操作符返回的是一个string(不可以对其取地址),故其为右值;
m是一个常量引用,引用到一个右值,但引用本身是一个持久对象(可以对其取地址),为左值


右值引用

右值引用就是一个对右值进行引用的类型。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。

举例:

int i=42;int &r=i;   //正确:r引用iint &&rr=i;   //错误:不能将一个右值引用绑定到一个左值上int &r2=i*42;    //错误:i*42是一个右值const int &r3=i*42;     //正确:我们可以将一个const的左值引用绑定到一个右值上int &&rr2=i*42;         //正确:将rr2绑定到乘法结果上

说明:

  • 返回左值引用的函数,连同赋值,下标,解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。
  • 返回非引用类型的函数,连同算数,关系,位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。


右值引用的作用

通过右值引用的定义,我们可以知道,右值引用只能绑定到一个将要销毁的对象。我们可以通过右值引用的声明,使该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。这样,我们可以通过该右值引用继续使用该右值的数据。

看一下下面的代码:

#include<iostream>using namespace std;int g_constructCount=0;int g_copyConstructCount=0;int g_destructCount=0;struct A{    A(){        cout<<"construct: "<<++g_constructCount<<endl;    }    A(const A& a)    {        cout<<"copy construct: "<<++g_copyConstructCount<<endl;    }    ~A()    {        cout<<"destruct: "<<++g_destructCount<<endl;    }};A GetA(){    return A();}int main(){   A a=GetA();   return 0;}

为了清楚地观察临界值,在GCC下编译时设置编译选项-fno-elide-constructors来关闭返回值优化结果(返回值优化将会把临时对象优化掉)。

运行结果:

construct: 1
copy construct: 1
destruct: 1
copy construct: 2
destruct: 2
destruct: 3

从上面例子中可以看到,在没有返回值优化的情况下,拷贝构造函数调用了两次,一次是GeTA()函数内部创建的对象返回后构造一个临时对象产生的,另一次是在main函数中构造a对象产生的。第二次的destruct是因为临时对象在构造a对象之后就摧毁了。

上述例子中,在main函数中创建了一个类A的对象a,并将GetA()返回的临时对象拷贝给a。如果在上面的代码中我们通过右值引用来绑定函数的返回值,结果又会怎么样呢?

#include<iostream>using namespace std;int g_constructCount=0;int g_copyConstructCount=0;int g_destructCount=0;struct A{    A(){        cout<<"construct: "<<++g_constructCount<<endl;    }    A(const A& a)    {        cout<<"copy construct: "<<++g_copyConstructCount<<endl;    }    ~A()    {        cout<<"destruct: "<<++g_destructCount<<endl;    }};A GetA(){    return A();}int main(){   const A &&a=GetA();   return 0;}

gcc file.c -fno-elide-constructors 运行结果如下:

construct: 1
copy construct: 1
destruct: 1
destruct: 2

我们会发现,通过右值引用,比之前少了一次拷贝构造函数和一次析构函数,原因在于右值引用绑定了右值,让临时右值的生命周期延长了。我们可以利用这个特点做一些性能优化,即避免临时对象的拷贝构造和析构。事实上,通过常量左值引用也经常用来做性能优化。将上面的代码改成:const A&a=GetA();
输出结果与右值引用一样,因为常量左值引用是一个“万能”的引用类型,既可以接受非常量左值,常量左值,又可以接受非常量右值,常量右值。


特例:自动类型推断时的引用折叠

当在发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),T&&并不一定表示右值,它绑定的类型是未定的,既可能是左值又可能是右值。
看看下面的例子:

template<class T>void f(T&& param);f(10);    //10是右值int x=10;f(x);    //x虽然是左值,但是正确

从上面的例子可以看出,param有时是左值,有时是右值。这表示param实际上是一个未定的引用类型,它是左值还是右值引用取决于它的初始化,如果T&&被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值。
当我们将10传入f函数时,因为10是int类型右值,所以T会被推导为int。
而当我们将左值x对象传入f函数时,T会被推导为int&类型,此时函数参数为int & &&param,此时就涉及到引用折叠,int& &&等价于int&类型。

引用折叠:

  • 所有的右值引用叠加到右值引用上仍然还是一个右值引用。(例:T&& &&折叠成T&&)
  • 所有的其他引用类型之间的折叠都将变成左值引用。 (例:T& &&折叠成T&)

需要我们注意的是,未定的引用类型只在T&&下发生,任何一点附加条件都会使之失效,而变成一个普通的右值引用。
比如:

template<class T>void f(const T&&param);int x=10;f(x);    //错误,此时param是一个右值引用,x为左值,无法传给param


std::move

通过上面的介绍,我们先看一下下面的代码:

int w1,w2;auto&& v1=w1;  //v1被推导为int&类型decltype(w1)&& v2=w2;  //错误

v1是一个未定的引用类型,它被一个左值初始化,所以它最终是一个左值;v2是一个右值引用类型,但它被一个左值初始化,一个左值初始化一个右值引用类型是不合法的,所以编译器会报错。
但是,如果我们希望把一个左值赋给一个右值引用类型该怎么做呢?
此时就用到了标准库move函数。

decltype(w1)&& v2=sstd::move(w2);   //正确

虽然我们不能将一个右值引用直接绑定到一个左值上,但我们可以显示地将一个左值转换为对应的右值引用类型。我们可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用。

我们知道,右值表示的是一个临时对象或即将被摧毁的对象,当我们调用move函数返回一个左值对象的右值引用类型时,就意味着承诺:除了对该对象赋值或摧毁它外,我们将不再使用它。我们不能对调用move后的对象的值做任何假设。

原创粉丝点击