如果函数模板形参具备T&&型别,并且T的型别系推导而来,或如果对象使用auto&&声明其型别,则该形参或对象就是个万能引用。
万能引用首先是一个引用,它既可以绑定到左值引用,也可以绑定到右值引用。
先看个例子:
extern "C"{
int printf(const char * format, ...);
}
template<typename T>
void func(T& val){ //左值引用
printf("Val = %d\n", val);
}
int main(){
int a = 520;
func(a);
func(521); //此句编译出错
return 0;
}
上述代码中,模板函数只能接受一个左值引用。我们想要模板既可以接受一个左值,又可以接受一个右值,应该怎么做?答案是使用万能引用:
extern "C"{
int printf(const char * format, ...);
}
template<typename T>
void func(T&& val){ //万能引用
printf("Val = %d\n", val);
}
int main(){
int a = 520;
func(a);
func(521);
return 0;
}
此时,若采用右值来初始化万能引用,就会得到一个右值引用。若采用左值来初始化万能引用,就会得到一个左值引用。
如果型别声明并不是精确地具备Type&&形式,或者型别推导并未发生,则Type&&就代表右值引用。
void fun(Obj&& param); //右值引用
Obj&& val1 = Obj(); //右值引用
auto&& val2 = var1; //万能引用
template<typename T>
void fun(std::vector<T>&& param); //右值引用
template<typename T>
void fun(T&& param); //万能引用
template<typename T>
void fun(const T&& param); //右值引用
template<class T>
class vector{
public:
void push_back(T&& x); //右值引用
... //因为这里的T不是在这里被推导的
template<class... Args>
void emplace_back(Args&&... args); //万能引用
}
template<typename MyTemplateType>
void someFunc(MyTemplateType&& param); //万能引用
万能引用并非一种新的引用类型,其实它是满足下面两个条件的语境中的右值引用:
例如:
template<typename T>
void fun(T&& param){
//如果实参param是左值,T会推导成为T&,即变为T& && param,触发引用折叠,变为T& param
//如果实参param是右值,T会推导成为T,即变为T&& param
}
上述模板型别推导过程中出现了T& &&这种东西,即“引用的引用”,实际上“引用的引用”在C++中是非法的,编译器使用引用折叠机制来处理这个问题。
引用折叠机制其实很简单:当编译器在引用折叠语境下生成引用的引用时,结果会变成单个引用。如果原始的引用中有任一引用为左值引用,则结果为左值引用。否则,结果为右值引用。
即有:
T& && param --> T& param
T&& & param --> T& param
T& & parma --> T& param
T&& && param --> T&& param
先来分析以下代码:
class A{
public:
A(const A& obj):val(obj.val){
std::cout<<"A copy constructor."<<std::endl;
}
A(A&& obj):val(std::move(obj.val)){
std::cout<<"A move constructor."<<std::endl;
}
A(int _val):val(_val){
std::cout<<"A common constructor."<<std::endl;
}
private:
int val;
};
class B{
public:
B(const A &obj):a(obj){
std::cout<<"B copy constructor."<<std::endl;
}
B(A&& obj):a(obj){ //这里需要注意的是右值引用其实是一个左值
std::cout<<"B move constructor."<<std::endl;
}
private:
A a;
};
int main(){
A obj1(5);
//A common constructor.
B obj2(obj1);
//A copy constructor.
//B copy constructor.
B obj3(std::move(obj1));
//A copy constructor. //因为右值引用是左值,所以A调用的仍然是复制构造函数
//B move constructor.
B obj4(5);
//A common constructor.
//A copy constructor.
//B move constructor.
return 0;
}
将代码稍作修改:
class A{
public:
A(const A& obj):val(obj.val){
std::cout<<"A copy constructor."<<std::endl;
}
A(A&& obj):val(std::move(obj.val)){
std::cout<<"A move constructor."<<std::endl;
}
A(int _val):val(_val){
std::cout<<"A common constructor."<<std::endl;
}
private:
int val;
};
class B{
public:
B(const A &obj):a(obj){
std::cout<<"B copy constructor."<<std::endl;
}
B(A&& obj):a(std::move(obj)){ //使用std::move将右值引用转化为右值
std::cout<<"B move constructor."<<std::endl;
}
private:
A a;
};
int main(){
A obj1(5);
//A common constructor.
B obj2(obj1);
//A copy constructor.
//B copy constructor.
B obj3(std::move(obj1));
//A move constructor. //使用std::move转为为右值,所以A调用的是移动构造函数
//B move constructor.
B obj4(5);
//A common constructor. //这里先调用A的普通构造函数生成一个临时对象ta,这是一个右值,即B(A&& obj)中的形参obj
//A move constructor. //将临时对象obj移动构造A
//B move constructor. //
return 0;
}
这里我们可以看到虽然A类存在一个接受参数整形val的构造函数,但B中没有接受整形val的构造函数。所以当B接受一个整形val进行构造时,需要先构造出一个A类的临时对象,然后调用一次A移动构造函数,还需调用一次A的析构函数。
有没有办法不生成这个临时对象呢?
你也许会说在B类中再加一个接收整形val的重载构造函数不就好了,就像下面这样:
B(int _val):a(_val){
}
这的确是一种方法,但是这种设计的可扩展性太差。对于那些有多个形参的函数,每个形参都需要一个左值和一个右值,从而重载函数的个数会呈几何级数增长。更为糟糕的是有些函数会有无穷多个形参,而每个形参都可能是左值或右值。
那么最佳的解决办法是什么呢?那就是万能引用加完美转发。
先看代码:
class A{
public:
A(const A& obj):val(obj.val){
std::cout<<"A copy constructor."<<std::endl;
}
A(A&& obj):val(std::move(obj.val)){
std::cout<<"A move constructor."<<std::endl;
}
A(int _val):val(_val){
std::cout<<"A common constructor."<<std::endl;
}
~A(){
std::cout<<"A destructor."<<std::endl;
}
private:
int val;
};
class B{
public:
template<typename T>
B(T&& param):a(std::forward<T>(param)){ //param是一个万能引用
std::cout<<"B constructor."<<std::endl;
}
private:
A a;
};
int main(){
A obj1(5);
//A common constructor.
B obj2(obj1);
//A copy constructor.
//B constructor.
B obj3(std::move(obj1));
//A move constructor.
//B constructor.
B obj4(5);
//A common constructor.
//B constructor.
return 0;
}
这样问题就解决了,而且代码比之前要简短了好多有木有。
std::forward所做的事情实际上与std::move类似。std::move无条件将实参强制转换为右值,而std::forward是有条件的强制类型转换:仅当其实参是使用右值完成初始化时,它才会执行向右值型别的强制类型转换。
另外需要注意的一点是,针对右值引用的最后一次使用实施std::move,针对万能引用的最后一次使用实施std::forward。否则可能会出问题。