在讲解移动构造和完美转发之前,我们需要先了解什么是右值引用。
但在讲解右值引用之前,我们也得知道左值和右值分别是什么,有什么区别。
目录
左值与右值
左值与左值引用
右值与右值引用
引用和右值引用的区别
移动构造
移动赋值
插入接口中的右值引用
完美转发
左值与右值
左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。
左值与左值引用
左值是一个表示数据的表迭式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
例如:
//左值,可以取它的地址
//a,b,*p都是左值
int a = 10;
const int b = 20;
int* p = &a;
*p = 100;
//这几个是对上面左值的引用
int& rp = a;
const int& rb = b;
int*& rp = p;
例如左值可以出现在等号的右边:
//右值
double x = 1.1, y = 2.2;
10;
x + y;
fmin(x, y);
return 0;
右值与右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
以下是一些右值:
//右值
double x = 1.1, y = 2.2;
10;//字面常量
x + y;//这个是个表达式,最终结果会存放在临时变量里,也是个右值
fmin(x, y);//函数返回值
return 0;
//以下是对上面右值的引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x + y);
但右值一定不能放到等号的左边:
//错误:右值不能放在等号左边
10 = 1;
x + y = 1;
fmin(x, y) = 1;
总结来说:左值和右值的主要区别是 是否能对这些值取地址。 能取地址是左值,不能取地址的是右值.
引用和右值引用的区别
在C++98中的普通引用与const引用在引用实体上的区别:
左值引用可以引用左值,但引用右值必须加上const.
//普通引用:可以引用左值
int a = 10;
int& ra = a;
int& rra = 10;//编译出错,因为10是右值
//加上const便可以引用右值.
const int& ra2 = 10;
const int& ra3 = a;
那么右值引用可以引用左值吗?
右值引用不可以引用左值,但可以引用move以后的左值.
int c = 10;
int&& rrc = c;//错误,右值引用不可引用左值
int&& rrc2 = move(c);//正确,右值引用可以引用move以后的左值
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& r1去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int&& rr1 = 10;
rr1 = 20;//可以对右值引用的值作修改
cout << &rr1 << endl;//也可以获取地址
const int&& rr2 = 10;
//rr2 = 20;//错误,rr2被const去引用了,不可以再被修改
cout << &rr2 << endl;
可以看到rr1和rr2的地址也成输出出来。
那么右值引用到底是用来干什么的呢?
先不说右值引用,就先说引用的价值是什么? 减少拷贝!
在有些函数中参数加了引用,便不必再拷贝一份新的了。
左值引用解决哪些问题:
1.做参数:a.减少拷贝,提高效率 b.做输出型参数
2.做返回值:a.减少拷贝,提高效率 b.引用返回,可以修改返回对象(operator[]).
但左值引用对于以下场景会很难处理
string to_string(int val);
它的内部一定是返回了一个string,这个时候如果利用左值引用,成为下面这样:
string& to_string(int val);
这样就肯定会报错,因为引用了之前函数里的返回值,而里面的返回值又是一个临时对象,出了作用域会被销毁,所以引用一个被清理的数据会直接报错了。
这种情况我们可以考虑输出型参数,即不再用返回值,改为以下这样:
string to_string(int val,string& ans);
这样最后我们直接用ans即可.但是这不太符合我们的使用习惯。按正常来说,应该是接收一下,而这个还需要我们传入一个参数作为结果,很不习惯。
C++右值引用就可以用来解决以上问题.
先来看下面一种现象:
我们应该知道,函数的返回值返回时会把自身的值拷贝给一个临时变量,这个临时变量再拷贝给上一层栈帧所接收的那个值。
所以一共会拷贝两次。但是编译器一般会直接优化为一次,即直接将返回值拷贝给上一层栈帧所接收的那个值。(str直接拷贝给了ret)
既然这样,那这个临时变量有什么意义呢?我们每次直接拷贝给最后接收的值不就行了吗?
那肯定不行,如果不是一开始就接收返回值,而是先定义的然后再接收就不可以了:
这样就必须借助临时变量了,而且一共拷贝了两次(拷贝构造一次,=拷贝赋值运算符一次)
了解了这个,那我们开始讲解移动构造。右值引用一般不是直接使用,而是结合着其它只是一起使用。
移动构造
这里右值也分为两种:
1.内置类型右值---纯右值
2.自定义类型右值 --- 将亡值
将亡值正如其名,将要消亡,像一些临时对象,匿名对象之类的,生命周期只有本身这么一行,出了之后消亡了。
放在之前,如果是深拷贝,我们还得老老实实的开空间,然后拷贝一份数据到这个对象里,即使生命周期很短。
那既然你是将亡值,反正都要走了,在临走之前,做一个交易,把你的资源转移给我然后再走。所以叫做移动构造.
看下面的代码,这样就能理解大概的含义了:
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
//string tmp(s._str);
//swap(s);
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
swap(s);
}
int main()
{
bit::string str1("hello");
bit::string str2(str1); // 拷贝构造
bit::string str3(move(str1)); // 移动构造,想要变成右值需要加上move
}
我们调试,当我们执行完str2即拷贝构造这条语句时,我们发现s1的数据已经完整的拷贝一份给到s2了.
这时候str1和str2都还在。
我们继续向下调试,当执行完str3这条移动构造语句时,不同的现象出现了:
可以发现,str3把str1里的资源转移了。由于上面对str1加上了move.
所以也要谨慎使用move.
移动构造和拷贝构造函数的区别是:在传值的场景下,移动构造减少了拷贝次数。
上面是移动构造的工作,而普通构造函数会多拷贝一次:
这样移动构造其实少了一次拷贝构造,
移动构造中我们为什么敢直接交换右值,而不用创建一份临时变量来保存?
因为它是个将亡值!马上就要没了,所以可以直接换,不影响下面的程序。
移动赋值
既然有了移动构造和拷贝构造,也存在移动赋值和拷贝赋值。
回到刚开始说的那种情况:
在编译器没有优化的情况下,会发生两次拷贝,这是在处理左值的情况下。
如果是右值的话,它们会进行两次移动(移动构造,移动赋值)
相比起来,右值的移动构造和移动赋值提高了效率。只进行了两次的资源转移。而左值的情况却整拷贝了两次资源!代价未免太大了.
以上才是右值引用的真正价值。不仅构造和赋值在用右值引用,插入接口也在用右值引用。
插入接口中的右值引用
上面所使用的右值引用 解决的是传值返回这些类型对象的问题。
C++11中,STL容器都提供右值版本。
插入过程中,如果传递对象是右值对象,那么就会进行资源转移,减少拷贝.
int main()
{
list<bit::string> lt;
bit::string s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("2222");
lt.push_back(std::move(s1));
return 0;
}
完美转发
模板中&&万能引用:
- 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
- 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力 。
我们看以下代码,看看输出是不是符合我们的预期结果,注释的为预期结果。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
输出结果:
我们发现,无论传入的是左值还是右值,最后都成为了左值。
这是因为右值引用的对象,再次传递时会退化成左值引用,其实这很好理解,上文我们提到过,右值一旦被引用就会被存在一个特定的地方并且可以取到地址,所以再次使用时编译器就会把它当成左值了。想要保持它的右值属性,就要使用完美转发。
std::forward会在传参过程中保留数据的原生类型。
所以传参时加上forward,就可以保留其右值属性.
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
这个时候我们再次运行:
便符合我们的预期了.
这样C++11中的右值引用就讲的差不多了。