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引用i
int &&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 & &¶m,此时就涉及到引用折叠,int& &&等价于int&类型。
引用折叠:
- 所有的右值引用叠加到右值引用上仍然还是一个右值引用。(例:T&& &&折叠成T&&)
- 所有的其他引用类型之间的折叠都将变成左值引用。 (例:T& &&折叠成T&)
需要我们注意的是,未定的引用类型只在T&&下发生,任何一点附加条件都会使之失效,而变成一个普通的右值引用。
比如:
template<class T>
void f(const T&¶m);
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后的对象的值做任何假设。