右值引用和移动语义
- 左值:表达式结束后仍然存在的持久对象,可取地址
- 变量、函数或者数据成员的名称
- 返回左值引用的表达式,如++x, x = 1
- 右值:表达式结束后就不存在的临时对象,不可取地址
- 非定义的一些字面量,如int a = 2中的‘2’、bool flag = true中的'ture'
- 返回非引用类型的表达式,如x++, x+1
为什么需要右值引用?
int a = 2; 这里的a是一个左值,记录的值为2,那我们需要使用2这个数量值的时候就可以通过访问a得到,而且可以通过a去对2这个数量值进行修改或者参与计算。左值是需要分配的地址,如果我们需要使用2这个数量值且不希望额外为其分配地址空间,那就需要用到右值引用的方法。
- 右值引用的初始化---&&
右值的引用就是对一个右值创建引用类型。因为右值是没有名字的,所以如果需要找到和使用它,我们需要通过引用,也就是别名的方式去定位。无论左值引用还是右值引用,都遵循引用的特征:引用不可以为空,创建必须初始化。
int a = 100;
int &aa = a; // 创建了一个左值的引用
int &&bb = a; // 错误,右值引用初始化不可以使用左值作为初始化的值,只能以右值作为初始化的条件
int &&bb = 100; // 这里创建了一个右值引用,bb的生命周期此时就和常规左值一样,作用域内一直存活
int &&cc = bb; // 错误,&&bb是右值引用类型,但bb本身此时已经是按照左值的生成方式存在,**引用本身是左值**
auto &&cc = a; // auto类型推导出来的是未定引用类型,可能是左值引用也可能是右值引用,取决于a实体
引用可以绑定什么?
- 左值->只能绑定到左值引用 int &a = 10; //错误
- 右值->可以绑定到右值引用 int &&a = 10;
- 右值->可以绑定到常左值引用 const &str = "这是右值";
- 右值引用的价值---支持移动语义
- 移动语义可以将资源(堆内存、系统对象等)通过浅拷贝的方式从一个对象转移到另一个对象,能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅提高程序的性能,消除临时对象创建销毁所带来的影响
- 移动语义的实现:移动拷贝构造 和 移动赋值运算符
- 移动构造
class A{
// 通过默认构造创建的对象只能借助赋值运算符实现对象间资源的转移,即浅拷贝,如果涉及堆空间,则容易产生二次析构的问题,因为两者的ptr值是相同的
A():ptr(new int(10))
{
cout << "Default constructor\n";
}
// 拷贝构造,参数使用一个左值引用
// A a;
// a = A(10); 内存里产生了两份数据
// 这里通过深拷贝的方式对已存在对象a中的资源重新分配了新的内存空间去保存new int(*a.ptr), 这就保证存在两个对象指向同一片内存;
// 但是代价是多分配了一块新内存,如果涉及的堆内存越大,那么其代价也越大
A(const A& a):ptr(new int(*a.ptr))
{
cout << "copy constructor\n";
}
// 移动构造,参数使用一个右值引用
// A a = A(10); 调用移动构造,内存中只有一份数据
// 这里采用了浅拷贝的方式,而没有使用new进行深拷贝,从而避免了额外的内存分配,这就是移动语义
A(A && a)::ptr(a.ptr)
{
a.ptr = nullptr;
cout << "move constructor\n";
}
//移动赋值运算符
// A a;
// a = A(12);
// 这里调用移动赋值运算法,采用浅拷贝的方式
&A operate=(A &&a)
{
ptr = a.ptr;
a.ptr = nullptr;
return *this;
}
~A()
{
cout << "destructor\n";
if(ptr)
delete ptr;
}
private:
int *ptr;
}
- 移动构造的是通过一个右值参数来匹配临时对象,C++11中提供了std::move可以将左值转为右值。
- 之前智能指针中提到的unique_ptr的转移方法,就是采用的move()方法,将左值变为右值,并将被转换值清空
万能引用和完美转发
万能引用 universal reference
如果一个变量或者参数被声明为T&& ,其中T是被推导的类型,那这个变量或者参数就是一个universal reference。T一定要是需要被推导的类型!
template <class T>
void print(T && t)
{
cout << "引用的模板类型\n";
send(t);
}
int a = 10;
print(a); //传入左值, a推导出是左值,得到左值引用
print((int)a); //已知道是int类型,无需推导,直接得到T&&是右值引用
int &b = a;
print(b); // 传入左值引用,左值引用也是左值,得到左值引用
print(forward<int>(b)); // 已知保持int类型,无需推导,T&&是右值引用
print(10); // 传入右值,推导出右值,得到右值引用
int &&c = 10;
print(c); // 传入右值引用,右值引用也是左值, 得到左值引用
如果print(param)中param的类型需要推导(没有明确给出),那么就根据其值性判断,左值则T&&作为左值引用;反之为右值引用 如果print(param)中的param不需要推导,比如 print((int)1)或者print(forward(a)),那么此时的T&&就不再是一个万能引用,而是一个右值引用
当我们通过万能引用,比如上面实例代码中print(T &&t)接收了一个参数的时候,会以左值引用或者右值引用的方式接收这个参数,但是当进入函数内部的时候,此时的参数t如果需要传递给别人send(t),这时候的t就是一个左值,因为在函数体里他是一个形参。那进入send之后,源参数的引用属性(不管左右)就丢丢失了。
!!注意:左值和右值代表的是“值类别”,引用是一种“值类型”,值性只有左右(将死值也是右值);值类别有很多,引用只是其中一种,指针也是一种类型。注意区分这两个概念
此时需要使用一个方法,来保持其引用属性,并向下层函数传递。
完美转发
是指在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征),将参数传递给函数模板中调用的另外一个函数。
int &&aa = 100; // 正确
int &&bb = aa; // 错误, aa此时在作用域内是左值形式的存在
int &&bb == std::forward<int>(aa); // 正确
标注库中forward的大致实现
template <class T>
void print(T && t)
{
cout << "完美转发对 T&& 判断";
send(forward<T>(t));
}
//这个forward处理输入为左值的情况
//若万能引用得到左值,则T = int &, 则 T &&折叠为 int &
//若万能引用得到右值,则T = int, 则 T && 为 int &&
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
//这个forward处理输入为右值的情况
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
return static_cast<T&&>(param);
}
总结
- 右值引用的概念是实现移动语义的基础;std::move和std::forward是实现移动语义的中间手段;移动语义的实现依靠的是基于右值引用实现的移动拷贝构造和移动赋值运算。
(1)在类型声明当中, “&&” 要不就是一个右值引用,要不就是一个万能引用。对于某个被推导的类型T
,万能引用总是以 T&&
的形式出现。
(2)引用折叠是会让 万能引用有时解析为 左值引用 有时解析为 右值引用 的根本机制。引用折叠只会在一些特定的可能会产生"引用的引用"场景下生效。这些场景包括模板类型推导,auto
类型推导, typedef
的形成和使用,以及decltype
表达式。
(3)std::move解决的问题是对于一个本身是左值的右值引用变量需要绑定到一个右值上,所以需要使用一个能够传递右值的工具;std::forward解决的问题是万能引用接收左右值参数后,进行参数转发时会存在二义性,因为参数都是左值性质,丢失了万能指针在初始化时的左值引用或右值引用属性,如果一个本身是左值的万能引用如果绑定在了一个右值上面,就通过forward(t)把它重新转换为右值。
(4) 移动语义和完美转发保持右值属性的目的就是为了能够满足移动构造和移动赋值运算的参数要求,从而避免默认拷贝构造带来的深拷贝问题和普通赋值运算的浅拷贝缺陷。