右值引用
右值
C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象。 所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。 右值是指临时的对象,它们只在当前的语句中有效。
右值引用概念
为了支持移动操作,C++11引入了一种新的引用类型-右值引用。就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。右值引用只能绑定到一个将要销毁的对象。
由于右值引用只能绑定到临时对象,我们得知:
所引用的对象将要被销毁
该对象没有其他用户
这两个特性意味着:使用右值引用的代码可以自身自由的接管所引用的对象的资源。
变量可以看作只有一个运算对象而没有运算符的表达式。
int i = 23;
int &&j = i;//错误:不能将一个右值引用绑定到一个左值上
int &&k = i * 3;//正确,i*3是一个右值
标准库move函数
move函数理解之前先要知道左值、左值引用、右值、右值引用的概念。
虽然不能将一个右值引用直接绑定到一个左值上,但可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用。
int i=6;
int &&l=std::move(i);
对比下面操作
第二段代码使用了move语句,而第一段代码没有使用move语句。输出的结果差异也很明显,第二段代码中,原来的字符串st已经为空,而第一段代码中,原来的字符串st的内容没有变化。
int main()
{
string st = "I'm a superman";
vector<string> vc;
vc.push_back(st);
cout << vc[0] << endl;
if (!st.empty())
cout << st << endl;
system("pause");
return 0;
}
int main()
{
string st = "I'm a superman";
vector<string> vc;
vc.push_back(move(st));
cout << vc[0] << endl;
if (!st.empty())
cout << st << endl;
system("pause");
return 0;
}
对象移动
提出
在很多情况下都会发生对象拷贝,比如利用reallocate重新分配内存的过程,在其中某些情况下,对象拷贝后就立即被销毁了。在这些情况下,移动而非拷贝对象会大幅度提升性能。
移动构造函数和移动赋值运算符
移动构造函数,参数是右值引用
StrVec::StrVec(StrVec &&s) noexcept //noexcept指名不抛出异常
:elements(s.elements), first_free(s.first_free),cap(s.cap)
{
s.elements = s.first_free = s.cap = nullptr; //销毁地动源后的对象
}
与拷贝构造函数不同,移动构造函数不分配任何新内存:它接管给定的StrVec中的内存。在接管内存后,它将给定对象中的指针都置为nullptr。
合成的移动操作
与处理拷贝构造函数和拷贝赋值运算符一样,编译器也会合成移动构造函数和移动赋值运算符。但是,合成移动构造的条件与合成拷贝操作的条件大不相同。
只有当一个类没有定义任何自己版本的拷贝控制成员(拷贝构造函数、拷贝赋值运算符、析构函数),且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
如果一个类定义了一个移动构造函数或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符被定义为删除的。
右值引用和成员函数
除了构造函数和赋值运算符之外,成员函数也可以同时提供拷贝版本和移动版本。例如:
void push_back(const X&); //拷贝:绑定到任意类型的X
void push_back(X&&); //移动:只能绑定到类型X的可修改的右值
关于此部分讲解很好的博文:https://www.cnblogs.com/qingergege/p/7607089.html
move源码实现(转载自:原文地址)
预备知识
1.引用折叠规则:
X& + & => X&
X&& + & => X&
X& + && => X&
X&& + && => X&&
2.函数模板参数推导规则(右值引用参数部分):当函数模板的模板参数为T而函数形参为T&&(右值引用)时适用本规则。
若实参为左值 U& ,则模板参数 T 应推导为引用类型 U& 。
(根据引用折叠规则, U& + && => U&, 而T&& ≡ U&,故T ≡ U& )
若实参为右值 U&& ,则模板参数 T 应推导为非引用类型 U 。
(根据引用折叠规则, U或U&& + && => U&&, 而T&& ≡ U&&,故T ≡ U或U&&,这里强制规定T ≡ U )
3.std::remove_reference为C++0x标准库中的元函数,其功能为去除类型中的引用。
std::remove_reference<U&>::type ≡ U
std::remove_reference<U&&>::type ≡ U
std::remove_reference<U>::type ≡ U
4.以下语法形式将把表达式 t 转换为T类型的右值(准确的说是无名右值引用,是右值的一种)
static_cast<T&&>(t)
5.无名的右值引用是右值
具名的右值引用是左值。
6.注:本文中 ≡ 含义为“即,等价于“。
std::move
函数功能:
std::move(t) 负责将表达式 t 转换为右值,使用这一转换意味着你不再关心 t 的内容,它可以通过被移动(窃取)来解决移动语意问题。
源码与测试代码
template<typename _Tp>
inline typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t)
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
#include<iostream>
using namespace std;
struct X {};
int main()
{
X a;
X&& b = move(a);
X&& c = move(X());
}
代码说明:
- 测试代码第9行用X类型的左值 a 来测试move函数,根据标准X类型的右值引用 b 只能绑定X类型的右值,所以 move(a) 的返回值必然是X类型的右值。
- 测试代码第10行用X类型的右值 X() 来测试move函数,根据标准X类型的右值引用 c 只能绑定X类型的右值,所以 move(X()) 的返回值必然是X类型的右值。
- 首先我们来分析 move(a) 这种用左值参数来调用move函数的情况。
- 模拟单步调用来到源码第3行,_Tp&& ≡ X&, __t ≡ a 。
- 根据函数模板参数推导规则,_Tp&& ≡ X& 可推出 _Tp ≡ X& 。
- typename std::remove_reference<_Tp>::type ≡ X 。
- typename std::remove_reference<_Tp>::type&& ≡ X&& 。
- 再次单步调用进入move函数实体所在的源码第4行。
- static_cast<typename std::remove_reference<_Tp>::type&&>(__t) ≡ static_cast<X&&>(a)
- 根据标准 static_cast<X&&>(a) 将把左值 a 转换为X类型的无名右值引用。
- 然后我们再来分析 move(X()) 这种用右值参数来调用move函数的情况。
- 模拟单步调用来到源码第3行,_Tp&& ≡ X&&, __t ≡ X() 。
- 根据函数模板参数推导规则,_Tp&& ≡ X&& 可推出 _Tp ≡ X 。
- typename std::remove_reference<_Tp>::type ≡ X 。
- typename std::remove_reference<_Tp>::type&& ≡ X&& 。
- 再次单步调用进入move函数实体所在的源码第4行。
- static_cast<typename std::remove_reference<_Tp>::type&&>(__t) ≡ static_cast<X&&>(X())
- 根据标准 static_cast<X&&>(X()) 将把右值 X() 转换为X类型的无名右值引用。
- 由9和16可知源码中std::move函数的具体实现符合标准,
- 因为无论用左值a还是右值X()做参数来调用std::move函数,
- 该实现都将返回无名的右值引用(右值的一种),符合标准中该函数的定义。