移动语义和完美转发都是通过C++的右值引用来实现的。关于什么是右值引用可以去看看C++ primer这本书。
1、移动语义
首先,很多人会想,为什么我们有了拷贝赋值以后为什么还要有移动语义呢。我们先来看一个例子,假设我们有个类A和类B(两个类都是class test类型,见下图
),假如我们想把类A赋值给类B(类A中有通过new分配的内存空间),传统的赋值拷贝是要再申请一块内存空间给B,然后再把A的那块动态分配空间中的内容拷贝到B的那块空间(见下图)。但是现在我们考虑一种情况,这种情况就是我把A的值赋给B之后我就不会再使用A了,这时候如果再执行深拷贝是不是有点浪费了。这时候移动语义就发挥作用了,我就把A的指针给B不就行了吗(相当于一个浅拷贝),这样比深拷贝快了很多。
class test{
public:
test& operator=(const test &A){
分配一个新的空间;
用memcpy拷贝A中的内容;
}
test& operator=(test &&A){
B指针 = A指针;
A指针置为空;
}
private:
指针;
};
2.完美转发
为什么需要完美转发这种东西呢,让我们先来看一下。首先,我们有个函数A,里面再调用函数B,这种函数调用很常见是吧。函数调用见下面的图。当我们传入一个右值引用参数到函数A的时候,我们再把参数a传入函数B会调用B的那个版本的函数呢,让我们来看一下。
#include<iostream>
using namespace std;
class test {
public:
int a;
};
void B(const test &a) {
cout << "B &" << endl;
}
void B(const test &&a) {
cout << "B &&" << endl;
}
void A(const test &&a) {
B(a);
}
int main() {
test a;
A(std::move(a));
return 0;
}
B &
由上面的结果可见并不是我们所想象的调用B &&版本,为什么会这样呢,因为虽然test &&a代表一个右值引用,但是a是一个左值,所以就会调用B &版本的函数。那么如果我们想调用B &&版本呢,我们可以怎么做。看下面代码。
#include<iostream>
using namespace std;
class test {
public:
int a;
};
template<typename m>
void B(const m &a) {
cout << "B &" << endl;
}
template<typename m>
void B(const m &&a) {
cout << "B &&" << endl;
}
template<typename m>
void A(m &&a) {
B(std::forward<m>(a));
}
int main() {
test a;
A(std::move(a));
A(a);
return 0;
}
B &&
B &
看,加上一个forward就可以实现了,是不是很神奇,那么我们接下来看看move函数和forward函数做了些什么呢。
3.引用折叠
在看实现之前我们还需要了解一下引用折叠这个东西。一共有四种引用折叠
X& &, X& &&, X&& &都折叠成类型X &
X&& &&折叠成X &&
让我们看几个例子
template<typename X>
void fun(X &&a){
cout<<"aa"<<endl;
}
1.
class test a;
fun(a) //这时候X的类型为test &
fun(std::move(a)) //这时候X的类型为test &&(其实按引用折叠的规则来看X也可以是test,但是我在下面测试的结果表明X为test &&)
4.std::move
move的作用是把参数转换成右值引用。我们看下它是怎么实现的呢。
template<class T>
typename remove_reference<T>::type&&
std::move(T&& a) noexcept
{
typedef typename remove_reference<T>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
以下为remove_reference可能的实现
template< class T > struct remove_reference {typedef T type;};
template< class T > struct remove_reference<T&> {typedef T type;};
template< class T > struct remove_reference<T&&> {typedef T type;};
首先让我们来看看remove_reference是干什么用的呢,可以看到它的作用其实就是把引用去掉,不管是左值引用还是右值引用。
然后再让我们看看move的实现,首先我们可以看见move还是的返回类型是一个右值引用,函数的实现很简单就是使用static_cast把参数a转换为右值引用类型。
5.forward
template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
return static_cast<S&&>(a);
}
然我们结合上个例子来看forward
#include<iostream>
using namespace std;
class test {
public:
int a;
};
template<typename m>
void B(const m &a) {
cout << "B &" << endl;
}
template<typename m>
void B(const m &&a) {
cout << "B &&" << endl;
}
template<typename m>
void A(m &&a) {
B(std::forward<m>(a));
}
int main() {
test a;
A(std::move(a));
A(a);
return 0;
}
当A的传入参数std::move(a)时,m为test &&(这时候X的类型为test &&(其实按引用折叠的规则来看X也可以是test,但是我在下面测试的结果表明X为test &&)类型,也就是forward中的S为test &&类型,当S为test &&类型时可见forward返回一个 test&& &&即test &&类型,可见传入函数A和函数B的参数类型编程一致了。
当A的传入参数为a时,m为test &类型,也就是forward中的S为test &类型,这时候返回类型为S &&即为test& &&,按照引用折叠的规则,返回forward的返回类型为test &,也就是B中的参数为test &,和传入A的参数类型一致。
6.测试:
#include<iostream>
using namespace std;
class test {
public:
test(int c):a(c){}
int a;
};
template<typename m>
void B(const m &a) {
cout << "B &" << endl;
}
template<typename m>
void B(const m &&a) {
cout << "B &&" << endl;
}
template<typename m>
void A(m &&a) {
test b(2);
B(static_cast<m>(b));
}
int main() {
test a(1);
A(std::move(a));
while (1);
return 0;
}
B &&