通用引用(万能引用)
通用引用通常表示为 T&&,其中 T 是一个模板参数类型(模板中的&&表示通用引用)。&& 虽然通常表示一个右值引用,但当T由模板参数推导而来时,其既可以绑定左值,又可以绑定右值。当 T 是一个模板参数类型,并且T是推导出来的类型,T&&才是通用引用。
万能引用 既可以接收左值,又可以接收右值。实参是左值,通用引用就是左值引用(引用折叠)实参是右值,通用引用就是右值引用。
(引用折叠 :当其传入参数为左值时,&&会折叠成&;当传入参数为右值时,&&不折叠照常接收)
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); return 0;
}
(2)模板类型推导
理解auto
的模板类型推导如果你不介意浏览少许伪代码,可以考虑像这样一个函数模板:
template<typename T>
voidf(ParamType param);
它的调用看起来像这样
f(expr); //使用表达式调用f
编译期间,编译器使用expr
进行两个类型推导:一个是针对T
的,另一个是针对ParamType
。这两个类型通常不同,因为ParamType
包含一些修饰,如const
和引用修饰符。
对于模板函数:
template<typename T>
void f(ParamType param);
编译器需要完成两步类型推导:
(1)推导 T 的类型
T是模板参数,当调用 f 函数并传递一个参数时,编译器会尝试根据该参数来确定 T 的具体类型。
如果调用 f(10),编译器会推断 T 是 int。
(2)推导 ParamType 的类型
ParamType 是函数参数的类型,它可以是 T 本身,也可以是对 T 进行了一些修饰后的类型,比如加上 const、&(引用)、*(指针)等。例如,假设定义如下函数模板
template<typename T>
void f(const T& param);
ParamType 是 const T&,T 的常量引用。如果调用 f(10),编译器会推断 T 是 int,ParamType 是 const int&。
template<typename T>
void f(const T& param) {// 函数体
}
调用示例1
int x = 10;
f(x);
编译器推导 T 为 int,ParamType 为 const int&。
调用示例 2
double y = 3.14;
f(y);
编译器推导 T 为 double,ParamType 为 const double&。
调用示例 3
std::string s = "hello";
f(s);
编译器推导 T 为 std::string,ParamType 为 const std::string&。
T 是模板参数,ParamType 是函数参数的实际类型,它可能是 T 本身,也可能是对 T 进行了一些修饰后的类型(如 const T&、T* 等)。编译器会根据你传递给函数的实际参数来推导 T 和 ParamType 的具体类型。
有三种情况:
ParamType
是一个指针或引用,但不是通用引用ParamType
是一个通用引用ParamType
既不是指针也不是引用
分情况讨论三种情况,每个情景基于之前给出的模板:
template<typename T>
voidf(ParamType param);
f(expr); //从expr中推导T和ParamType
情景一:ParamType是一个指针或引用,但不是通用引用
在这种情况下,类型推导会这样进行:
- 如果
expr
的类型是一个引用,忽略引用部分。 - 然后
expr
的类型与ParamType
进行模式匹配来决定T
。
template<typename T>
voidf(T& param); //param是一个引用
我们声明这些变量,
int x=27;//x是int
const int cx=x;//cx是const int
const int& rx=x;//rx是指向作为const int的x的引用
在不同的调用中,对param
和T
推导的类型会是这样:
f(x); //T是int,param的类型是int&
f(cx); //T是const int,param的类型是const int&
f(rx); //T是const int,param的类型是const int&
在第二个和第三个调用中,因为cx
和rx
被指定为const
值,所以T
被推导为const int
,从而产生了const int&
的形参类型。这对于调用者来说很重要。当他们传递一个const
对象给一个引用类型的形参时,他们期望对象保持不可改变性,也就是说,形参是reference-to-const
的。
这也是为什么将一个const
对象传递给以T&
类型为形参的模板安全的:对象的const
会被保留为T
的一部分。
在第三个例子中,即使rx
的类型是一个引用,T
也会被推导为一个非引用 ,这是因为rx
的引用性在类型推导中会被忽略。
如果我们将f
的形参类型T&
改为const T&
,情况有所变化。cx
和rx
的const
依然被遵守,但是因为现在假设param
是reference-to-const
,const
不再被推导为T
的一部分:
template<typename T>
voidf(const T& param);//param现在是reference-to-constint x = 27; //如之前一样
const int cx = x;//如之前一样constint& rx = x;
//如之前一样
f(x); //T是int,param的类型是const int&
f(cx); //T是int,param的类型是const int&
f(rx); //T是int,param的类型是const int&
同之前一样,rx
的reference-ness在类型推导中被忽略了。如果param
是一个指针(或者指向const
的指针)而不是引用,情况本质上也一样:
template<typename T>
voidf(T* param); //param现在是指针
int x = 27;//同之前一样
const int *px = &x;//px是指向作为const int的x的指针
f(&x);//T是int,param的类型是int*
f(px);//T是const int,param的类型是const int*
情景二:ParamType是一个通用引用
模板使用通用引用形参的话,那事情就不那么明显了。这样的形参被声明为像右值引用一样(也就是,在函数模板中假设有一个类型形参T
,那么通用引用声明形式就是T&&
),它们的行为在传入左值实参时大不相同。
- 如果
expr
是左值,T
和ParamType
都会被推导为左值引用。第一,这是模板类型推导中唯一一种T
被推导为引用的情况。第二,虽然ParamType
被声明为右值引用类型,但是最后推导的结果是左值引用。 - 如果
expr
是右值,就使用正常的推导规则
template<typename T>
voidf(T&& param); //param现在是一个通用引用类型
int x=27; //如之前一样
const int cx = x; //如之前一样
const int & rx =cx;//如之前一样
f(x); //x是左值,所以T是int&,//param类型也是int&
f(cx); //cx是左值,所以T是const int&,//param类型也是const int&
f(rx); //rx是左值,所以T是const int&,//param类型也是const int&
f(27); //27是右值,所以T是int,param类型就是int&&
情景三:ParamType既不是指针也不是引用
当ParamType
既不是指针也不是引用时,通过传值的方式处理。
template<typename T>
voidf(T param); //以传值的方式处理param
无论传递什么,param
都会成为它的一份拷贝一个完整的新对象。param
成为一个新对象这一行为会影响T
如何从expr
中推导出结果。如果expr
的类型是一个引用,忽略这个引用部分。如果忽略expr
的引用性之后,expr
是一个const
,那就再忽略const
。如果它是volatile
,也忽略volatile
int x=27; //如之前一样
const int cx=x; //如之前一样
const int & rx=cx; //如之前一样
f(x); //T和param的类型都是int
f(cx); //T和param的类型都是int
f(rx); //T和param的类型都是int
注意即使cx
和rx
表示const
值,param
也不是const
。param
是一个完全独立于cx
和rx
的对象——是cx
或rx
的一个拷贝。具有常量性的cx
和rx
不可修改并不代表param
也是一样。这就是为什么expr
的常量性在推导param
类型时会被忽略:因为expr
不可修改并不意味着它的拷贝也不能被修改。
只有在传值给形参时才会忽略const(volatile
),对于reference-to-const
和pointer-to-const
形参来说,expr
的const
在推导时会被保留。
如果expr
是一个const
指针,指向const
对象,expr
通过传值传递给param
:
template<typename T>
voidf(T param); //仍然以传值的方式处理param
const char* const ptr = ".." //ptr是一个常量指针
f(ptr); //传递const char * const类型的实参
在这里,解引用符号(*)的右边的const
表示ptr
本身是一个const
:ptr
不能被修改为指向其它地址,也不能被设置为null(解引用符号左边的const
表示ptr
指向一个字符串,这个字符串是const
,因此字符串不能被修改)。
当ptr
作为实参传给f
,组成这个指针的每一比特都被拷贝进param
。
ptr
自身的值会被传给形参,根据类型推导的第三条规则,ptr
自身的常量性const
将会被省略,所以param
是const char*
,一个可变指针指向const
字符串。在类型推导中,这个指针指向的数据的常量性const
将会被保留,但是当拷贝ptr
来创造一个新指针param
时,ptr
自身的常量性const
将会被忽略。
数组实参
比如数组类型不同于指针类型,虽然它们两个有时候是可互换的。关于这个错觉最常见的例子是,在很多上下文中数组会退化为指向它的第一个元素的指针。
constchar name[] = "J. P. Briggs"; //name的类型是const char[13]
constchar * ptrToName = name; //数组退化为指针
在这里const char*
指针ptrToName
会由name
初始化,而name
的类型为const char[13]
,这两种类型(const char*
和const char[13]
)是不一样的,但是由于数组退化为指针的规则,编译器允许这样的代码。
但要是一个数组传值给一个模板会怎样?会发生什么?
template<typename T>
void f(T param); //传值形参的模板
f(name); //T和param会推导成什么类型?
有一个函数的形参是数组,是的,这样的语法是合法的,
void myFunc(int param[]);
但是数组声明会被视作指针声明,这意味着myFunc
的声明和下面声明是等价的:
void myFunc(int* param); //与上面相同的函数
数组与指针形参这样的等价是C语言的产物,C++又是建立在C语言的基础上,它让人产生了一种数组和指针是等价的的错觉。因为数组形参会视作指针形参,所以传值给模板的一个数组类型会被推导为一个指针类。在模板函数f
的调用中,它的类型形参T
会被推导为const char*
:
f(name); //name是一个数组,但是T被推导为const char*
虽然函数不能声明形参为真正的数组,但是可以接受指向数组的引用,修改f
为传引用:
template<typename T>
voidf(T& param); //传引用形参的模板
我们这样进行调用
f(name); //传数组给f
T
被推导为了真正的数组!这个类型包括了数组的大小,在这个例子中T
被推导为const char[13]
,f
的形参(该数组的引用)的类型则为const char (&)[13]
。这种语法看起来又臭又长,但是知道它将会让你在关心这些问题的人的提问中获得大神的称号。
有趣的是,可声明指向数组的引用的能力,使得我们可以创建一个模板函数来推导出数组的大小:
//在编译期间返回一个数组大小的常量值
constexpr std::size_t arraySize(T (&)[N]) noexcept return N;
}
在Item15提到将一个函数声明为constexpr
使得结果在编译期间可用。这使得我们可以用一个花括号声明一个数组,然后第二个数组可以使用第一个数组的大小作为它的大小,就像这样:
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; //keyVals有七个元素int maint Vals=[arraySize(keyVals)]; //mappedVals也有七个
std::array<int, arraySize(keyVals)> mappedVals; //mappedVals的大小为7
至于arraySize
被声明为noexcept
,会使得编译器生成更好的代码,具体的细节请参见Item14。
函数实参
在C++中不只是数组会退化为指针,函数类型也会退化为一个函数指针,我们对于数组类型推导的全部讨论都可以应用到函数类型推导和退化为函数指针上来。结果是:
void someFunc(int, double); //someFunc是一个函数,//类型是void(int, double)
template<typename T>
void f1(T param); //传值给f1template<typename T>
void f2(T & param); //传引用给f2
f1(someFunc);//param被推导为指向函数的指针,//类型是void(*)(int, double)
f2(someFunc); //param被推导为指向函数的引用,//类型是void(&)(int, double)
如果数组可以退化为指针,函数也会退化为指针。auto
依赖于模板类型推导。然而,数组和函数退化为指针把这团水搅得更浑浊。有时你只需要编译器告诉你推导出的类型是什么。这种情况下,翻到item4,它会告诉你如何让编译器这么做。