C++右值引用相关介绍
1.左值和右值
- 左值:表达式结束后依然存在的持久对象,能对其取地址,所有的具名变量和对象;
- 右值:表达式结束是就不存在的临时对象,不能对其取地址,不具名;
2. 右值
右值有两个概念构成,一个是纯右值,一个是将亡值;
- 纯右值:非引用返回的临时变量,运算表达式产生的临时变量,原始字面量和lamada表达式;
- 将亡值:与右值引用相关的表达式,将要被移动的对象,T&&函数的返回值,std::move返回值和转换为T&&的类型的转换函数的返回值。
C++ 中所有的值必须是左值,纯右值,将亡值三者之一。
3 &&的特性
无论是左值引用&还是右值引用都必须对其立即初始化。
右值引用可以延长临时右值的生命周期。
#include<iostream>
using namespace std;
int g_con = 0;
int g_copy = 0;
int g_des = 0;
struct A{
A(){ cout << "Construct:" << ++g_con << endl; }
A(const A& a){ cout << "CopyConstruct: " << ++g_copy << endl; }
~A(){ cout << "Destruct:" << ++g_des << endl; }
};
A GetA()
{
return A();
}
int main()
{
A a = GetA();
return 0;
}
关掉编译器的性能优化后,输出结果如下
Construct: 1
CopyConstruct: 1
Destruct:1
CopyConstruct: 2
Destruct:2
Destruct:3
GetA()函数内生成临时对象调用一次构造函数,让后将结果复制给GetA()的返回值调用一次复制构造函数,然后析构该临时对象;GetA()的临时对象调用复制构造函数赋值给a,析构该临时对象;程序结束析构对象a。
右值引用可以延长临时右值的生命周期,则将main函数改为如下:
int main()
{
A&& a = GetA();
return 0;
}
输出结果为:
Construct: 1
CopyConstruct: 1
Destruct:1
Destruct:2
右值引用绑定了右值让临时右值的生命周期延长。
T&&不一定表示右值或者左值,绑定类型为定。这种未定的引用类型称为universal reference。只有发生类型推断时&&才是universal reference。
template<typename T>
void f(T&& para)
f(10);//右值
int x=10;
f(x);//左值
左值或者右值是独立于类型的,右值引用可能是左值也可能是右值
int&& var1=x;//var1的类型是int&&,var1是左值
auto&& var2=var1;//var2是左值,类型是 int&
int w1,w2;//w1,w2类型为int,是左值
auto&& v1=w1;v1的类型是int&&,v1是左值
decltype(w1)&& v2=w2; //v2的类型是int&&,而将左值初始化右值引用类型是不合法的——这里会报错
&&总结:
- 左值和右值是独立于他们的类型的,右值引用可能是左值也可能是右值;
- auto&& 或函数类型自动推导的T&& 是一个未定的引用类型,他可能是左值引用可能是右值引用类型,取决于初始化类型。
- 所以的右值引用叠加到右值引用上仍然是一个右值引用,而其他类型的引用折叠都为左值引用。当T&&为模板参数使,输入左值,他会变成左值引用,而输入右值则变为具名的右值引用。
- 编译器会将已命名的右值引用视为左值,为将为命名的右值引用视为右值。
4.右值引用优化性能,避免深拷贝
当class中含有堆内存时(内对象中的数据成员是new出来的),必须实现深拷贝,也就是重新定义一个copy构造函数,复制指针变量。
定义一个移动构造函数来避免深拷贝,移动构造函数将对象资源做浅拷贝,避免了额外的拷贝。移动构造函数的参数的一个右值引用类型参数T&&,T&&根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。
移动语义(move语义):通过浅拷贝的方式从一个对象转移到另一个对象,这样能减少不必要的临时对象的创建、拷贝及销毁。
一个例子说明拷贝构造函数、赋值操作符和移动构造函数和移动赋值函数
class Mystring{
private:
char* m_data;
size_t m_len;
void copy_data(const char* s){
m_data=new char[m_len+1];
memcpy(m_data,s,m_len);
}
public:
Mysting(){
m_data=NULL;
m_len=0;
}
Mysting(const char *p){
m_len=strlen(p);
copy_data(p);
}
Mysting(const Mystring& str){ //拷贝构造函数
m_len=str.m_len;
copy_data(p);
}
Mysting& operator=(const Mystring& str){ //赋值函数
if(this!=&str)
{
m_len=str.m_len;
copy_data(str.m_data);
}
return this;
}
Mysting(Mystring&& str){ //移动构造函数,避免了不必要的拷贝
m_len=str.m_len;
m_data=str.m_data;
str.m_len=0;
str.m_data=nullptr;
}
Mysting& operator=(Mystring&& str){ //移动赋值函数
if(this!=&str)
{
m_len=str.m_len;
m_data=str.m_data;
str.m_len=0;
str.m_data=nullptr;
}
return this;
}
};
void test(){
Mystring a;
a=Mystring("Holle");
std::vector<Mystring> vec;
vec.push_back(Mystring("World"));
}
根据参数的左值还是有值,选择拷贝或者移动构造函数。
通常在提供右值引用的移动构造函数时还会提供常量左值引用的拷贝构造函数。
5. move语义
在move语义之前,当我们对一个对象内部含有指向资源的指针进行拷贝时,需要这样定义拷贝构造函数
class A{
int* m_ptr;
};
A& A::operator=(const A& rhs)
{
//销毁m_ptr指向的资源
//复制rhs.m_ptr所指的资源,并使m_ptr指向它
}
A foo();
A a;
a=foo(); //销毁a所指的资源
//复制foo返回的临时对象的资源
//销毁临时对象的资源
如果采用move语义,直接交换临时对象与变量的资源,然后让临时对象的析构函数销毁他,这样可以避免复制造成的资源浪费。
上例加上移动赋值操作符
A& A::operator=(A&& rhs)
{
//交换资源,无需复制
}
当一个对象内部有较大的堆内存或者动态数组时,需要写move语义的移动构造函数和移动赋值函数。
6.forward 和完美转发
完美转发是指在函数模板中,完全依照模板的参数类型(保持参数的左值,右值特征),将参数传递给函数模板中调用的另一个函数。
emplace_back
用emplace_back代替push_back,它避免了拷贝和移动,可以直接用构造函数的参数构造对象。