左值引用vs右值引用
左值引用:&
int num = 10;
int &b = num;
int &c = 10;
右值引用:&& 和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:
int num = 10;
int && a = num;
int && a = 10;
移动构造函数
在 C++ 11 标准之前,如果想用其它对象初始化一个同类的新对象,只能借助类中的拷贝构造函数 。当类中拥有指针类型 的成员变量时,拷贝构造函数中需要以深拷贝 的方式复制该指针成员。
看下面的例子:
#include <iostream>
using namespace std;
class demo{
public:
demo():num(new int(0)){
cout<<"construct!"<<endl;
}
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
private:
int *num;
};
demo get_demo(){
return demo();
}
int main(){
demo a = get_demo();
return 0;
}
如上所示,我们为 demo 类自定义了一个拷贝构造函数。该函数在拷贝 d.num 指针成员时,必须采用深拷贝的方式,即拷贝该指针成员本身的同时,还要拷贝指针指向的内存资源。否则一旦多个对象中的指针成员指向同一块堆空间,这些对象析构时就会对该空间释放多次,这是不允许的。
执行结果如下:
construct! <-- 执行 demo()
copy construct! <-- 执行 return demo()
class destruct! <-- 销毁 demo() 产生的匿名对象
copy construct! <-- 执行 a = get_demo()
class destruct! <-- 销毁 get_demo() 返回的临时对象
class destruct! <-- 销毁 a
由结果可以得出,利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次深拷贝操作。
怎样才能避免深拷贝导致的效率问题呢?——移动语义
什么是移动语义? 所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。而移动构造函数就是现实移动语义的具体方式
以前面程序中的 demo 类为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。当使用 get_demo() 函数返回的临时对象初始化 a 时,我们只需要将临时对象的 num 指针直接浅拷贝给 a.num,然后修改该临时对象中 num 指针的指向(通常另其指向 NULL),这样就完成了 a.num 的初始化。
下面程序对 demo 类进行了修改:
#include <iostream>
using namespace std;
class demo{
public:
demo():num(new int(0)){
cout<<"construct!"<<endl;
}
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
demo(demo &&d):num(d.num){
d.num = NULL;
cout<<"move construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
private:
int *num;
};
demo get_demo(){
return demo();
}
int main(){
demo a = get_demo();
return 0;
}
可以看到,在之前 demo 类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。
输出结果为:
construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!
通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象 初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数 完成。
临时对象既无名称也无法获取其存储地址,所以属于右值,可以初始化右值引用
结论: 当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数 来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数 。
我们在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。
move函数
上面讲到,当使用类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数 那么,用当前类的左值对象(有名称,能获取其存储地址的实例对象)初始化同类对象时,是否就无法调用移动构造函数了呢?当然不是,C++11 标准中已经给出了解决方案,即调用 move() 函数。
move 的功能很简单,就是将某个左值强制转化为右值。
#include <iostream>
using namespace std;
class movedemo{
public:
movedemo():num(new int(0)){
cout<<"construct!"<<endl;
}
movedemo(const movedemo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
movedemo(movedemo &&d):num(d.num){
d.num = NULL;
cout<<"move construct!"<<endl;
}
public:
int *num;
};
int main(){
movedemo demo;
cout << "demo2:\n";
movedemo demo2 = demo;
cout << "demo3:\n";
movedemo demo3 = std::move(demo);
return 0;
}
程序执行结果为:
construct!
demo2:
copy construct!
demo3:
move construct!
通过观察程序的输出结果,以及对比 demo2 和 demo3 初始化操作不难得知,demo 对象作为左值,直接用于初始化 demo2 对象,其底层调用的是拷贝构造函数;而通过调用 move() 函数可以得到 demo 对象的右值形式,用其初始化 demo3 对象,编译器会优先调用移动构造函数。
完美转发
什么是完美转发? 它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
举个不完美转发的例子:
template<typename T>
void function(T t) {
otherdef(t);
}
显然,function() 函数模板并没有实现完美转发。一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。总之,无论从那个角度看,function() 函数的定义都不“完美”。
怎么才能让上面的function() 函数模板实现完美转发呢? C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。 借助右值引用,在 C++11 标准中实现完美转发,只需要编写如下一个模板函数即可:
template <typename T>
void function(T&& t) {
otherdef(t);
}
通过将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。 但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢?
C++11 标准的开发者已经帮我们想好的解决方案,该新标准还引入了一个模板函数 forword(),我们只需要调用该函数,就可以很方便地解决此问题。
实现完美转发的函数模板:
template <typename T>
void function(T&& t) {
otherdef(forward<T>(t));
}
此 function() 模板函数才是实现完美转发的最终版本。可以看到,forword() 函数模板用于修饰被调用函数中需要维持参数左、右值属性的参数。
|