一、模板实参推断
对于函数模板,编译器利用调用中的函数实参来确定其模板参数。从函数实参来确定模板实参的过程被称为模板实参推断。在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数与给定的函数调用最为匹配。
1、类型转换与模板类型参数
与非模板函数一样,我们在一次调用中传递给函数模板的实参被用来初始化函数的形参。如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。只有很有限的几种类型转换会自动地应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。
顶层const无论是在形参中还是在实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括如下两项:
- const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。
- 数组或函数指针转换:如果形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。
其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都是不能应用于函数模板。
1 #include2 #include 3 #include 4 #include 5 #include 6 7 template 8 void fobj(T x, T y) { // 实参被拷贝 9 std::cout << __FUNCTION__ << std::endl;10 }11 template 12 void fref(const T &x, const T &y) { // 引用13 std::cout << __FUNCTION__ << std::endl;14 }15 int main()16 {17 std::string s1("hi");18 const std::string s2("hello");19 fobj(s1, s2); // 调用fobj(string,string),const被忽略20 fref(s1, s2); // 调用fref(const string&,const string&),将s1转换成const是允许的21 22 int a[10], b[10];23 fobj(a, b); // 调用f(int*,int*)24 //fref(a, b); // 错误:数组类型不匹配25 return 0;26 }
1)使用相同模板参数类型的函数形参
一个模板类型参数可以用作多个函数形参的类型。由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。如果推断出的类型不匹配,则调用就是错误的。
1 #include2 #include 3 #include 4 #include 5 #include 6 7 template 8 bool compare(const T &x, const T &y) { 9 std::cout << __FUNCTION__ << std::endl;10 return x == y;11 }12 int main()13 {14 compare(1, 2.2); // 错误:类型不匹配,一个是int,一个是double15 return 0;16 }
2)正常类型转换应用于普通函数实参
函数模板可以有用普通类型定义的参数,即,不涉及模板类型参数的类型。这种函数实参不进行特殊处理:它们正常转换为对应形参的类型。
1 #include2 #include 3 #include 4 #include 5 #include 6 7 template 8 bool compare(const double &x, const T &y) { 9 std::cout << __FUNCTION__ << std::endl;10 return x == y;11 }12 int main()13 {14 compare(1, 2.2); // 正确:1是int,转换为double15 return 0;16 }
2、函数模板显式实参
在某些情况下,编译器无法推断出模板实参的类型。其他一些情况下,我们希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。
1)指定显式模板实参
1 #include2 #include 3 #include 4 #include 5 #include 6 7 template 8 T1 sum(const T2 &x, const T3 &y) { // 编译器无法推断T1,它未出现在函数参数列表中 9 return x + y;10 }11 int main()12 {13 auto ret = sum (1, 2.2);14 std::cout << ret << std::endl;15 return 0;16 }
在本例中,没有任何函数实参的类型可用来推断T1的类型。每次调用sum时调用者都必须为T1提供一个显式模板实参。
我们提供显式模板实参的方式与定义类模板实例的方式相同。显式模板实参在尖括号中给出,位于函数名之后,实参列表之前。
显式模板实参按从左至右的顺序与对应的模板实参匹配:第一个模板实参与第一个模板参数匹配,第二个实参与第二个参数匹配,依次类推。只有尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。
2)正常类型转换应用于显式指定的实参
对于模板参数已经显式指定了的函数实参,也进行正常的类型转换。
1 #include2 #include 3 #include 4 #include 5 #include 6 7 template 8 bool compare(const T &x, const T &y) { 9 std::cout << __FUNCTION__ << std::endl;10 return x == y;11 }12 int main()13 {14 //compare(1, 2.2); // 错误:类型不匹配,一个是int,一个是double15 compare (1, 2.2); // 正确16 return 0;17 }
3、尾置返回类型与类型转换
1 #include2 #include 3 #include 4 #include 5 #include 6 7 // 由于尾置返回出现在参数列表之后,它可以使用函数的参数 8 template 9 auto fcn(It beg, It end)->decltype(*beg) {10 std::cout << __FUNCTION__ << std::endl;11 return *beg; // 返回序列中一个元素的引用12 }13 int main()14 {15 std::vector v = { 1,2,3 };16 std::cout << fcn(v.begin(), v.end()) << std::endl;17 return 0;18 }
1)进行类型转换的标准库
有时,我们希望编写一个类似fcn的函数,但返回一个元素的值而非引用。为了获得元素的类型,我们可以使用标准库的类型转换模板。
对Mod<T>,其中Mod为 | 若T为 | 则Mod<T>::type为 |
remove_reference | X&或X&& 否则 | X T |
add_const | X&、const X或函数 否则 | T const T |
add_lvalue_reference | X& X&& 否则 | T X& T& |
add_rvalue_reference | X&或X&& 否则 | T T&& |
remove_pointer | X* 或者 | X T |
add_pointer | X&或X&& 否则 | X* T* |
make_signed | unsigned X 否则 | X T |
make_unsigned | 带符号类型 否则 | unsigned X T |
remove_extent | X[n] 否则 | X T |
remove_all_extents | X[n1][b2]... 否则 | X T |
每个模板都有一个名为type的public成员,表示一个类型。此类型与模板自身的模板类型相关,其关系如模板名所示。如果不可能(或者不必要)转换模板参数,则type成员就是模板参数类型本身。
1 #include2 #include 3 #include 4 #include 5 #include 6 7 // 由于尾置返回出现在参数列表之后,它可以使用函数的参数 8 template 9 auto fcn(It beg, It end)->typename std::remove_reference ::type {10 std::cout << __FUNCTION__ << std::endl;11 return *beg; // 返回序列中一个元素的拷贝12 }13 int main()14 {15 std::vector v = { 1,2,3 };16 std::cout << fcn(v.begin(), v.end()) << std::endl;17 return 0;18 }
4、函数指针和实参推断
当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
1 #include2 #include 3 #include 4 #include 5 #include 6 7 template 8 int compare(const T &x, const T &y) { 9 if (x < y) return -1;10 if (x > y) return 1;11 return 0;12 }13 int main()14 {15 int(*pf)(const int&, const int&) = compare;16 std::cout << pf(2, 3) << std::endl;17 return 0;18 }
pf中参数的类型决定了T的模板实参的类型。在本例中,T的模板实参类型为int。
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。
5、模板实参推断和引用
为了理解如何从函数调用进行类型推断,考虑下面的例子:
template<typename T> void f(T &p);
其中函数参数p是一个模板类型参数T的引用,非常重要的是记住两点:编译器会应用正常的引用绑定规则;const是底层的,不是顶层的。
1)从左值引用函数参数推断类型
当一个函数参数是模板类型参数的一个左值引用时,绑定规则告诉我们,只能传递给它一个左值(如,一个变量或一个返回引用类型的表达式)。实参可以是const类型,也可以不是。如果实参是const的,则T将被推断为const类型。
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 8 template void f(T &p) { 9 std::cout << __FUNCTION__ << std::endl;10 }11 int main()12 {13 int i = 1;14 const int ci = 2;15 f(i); // T是int16 f(ci); // T是const int17 // f(5); // 错误:必须是一个左值才可以18 return 0;19 }
如果一个函数参数的类型是const T&,正常的绑定规则告诉我们可以传递给它任何类型的实参——一个对象(const或非const)、一个临时对象或是一个字面值常量值。当函数本身是const时,T的类型推断的结果不会是一个const类型。const已经是函数参数类型的一部分;因此,它不会也是模板参数类型的一部分。
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 8 template void f(const T &p) { 9 std::cout << __FUNCTION__ << std::endl;10 }11 int main()12 {13 int i = 1;14 const int ci = 2;15 f(i); // T是int16 f(ci); // T是int17 f(5); // 一个const &参数可以绑定到一个右值18 return 0;19 }
2)从右值引用函数参数推断类型
当一个函数参数是一个右值引用时,正常绑定规则告诉我们可以传递给它一个右值。推断出的T的类型是该右值实参的类型。
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 8 template void f(T &&p) { 9 std::cout << typeid(p).name() << std::endl;10 }11 int main()12 {13 f(1024); // 实参是一个int类型的右值,T是int14 return 0;15 }
3)引用折叠和右值引用参数
template<typename T> void f(T &&p);
假定i是一个int对象,我们可能认为f(i)这样的调用是不合法的。毕竟,i是一个左值,而我们通常不能将一个右值引用绑定到一个左值上。但是,C++语言在正常绑定规则之外定义了两个例外规则,允许这种绑定。这两个例外规则是move这种标准库设施正确工作的基础。
第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型。因此,当我们调用f(i)时,编译器推断T的类型为int&。
T被推断为int&看起来好像意味着f的函数参数应该是一个类型int&的右值引用。通常,我们不能直接定义一个引用的引用。但是通过类型别名或通过模板类型参数间接定义是可以的。
在这种情况下,我们可以使用第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到右值引用。只有一种特殊情况下引用会折叠成右值引用;右值引用的右值引用。即,对于一个给定类型X:
- X& &、X& &&和X&& &都折叠成类型X&。
- 类型X&& &&折叠成X&&。
引用折叠只能应用于间接创建的引用,如类型别名或模板参数。
这个规则导致:如果一个函数参数是接受模板参数类型的右值引用(如T&&),则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用(T&)。
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 8 template void f(T &&p) { 9 p = 1024;10 }11 int main()12 {13 int x = 256;14 f(x);15 std::cout << x << std::endl;16 return 0;17 }
4)编写接受右值引用参数的模板函数
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 8 template void f(T &&p) { 9 T t = p; // 绑定引用10 t = 1024;11 if (t == p)12 std::cout << "T is &" << std::endl;13 }14 int main()15 {16 int x = 1;17 f(x);18 return 0;19 }
6、理解std::move
7、转发
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const以及实参是左值还是右值。
1)定义能保持类型信息的函数参数
通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。而使用引用参数(无论是左值还是右值)使得我们可以保持const属性,因为在引用类型中的const是底层的。如果我们将函数参数定义为T&&,通过引用折叠就可以保持实参的左值/右值属性。
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 8 void g(int &&i) { 9 std::cout << __FUNCTION__ << ", " << i << std::endl;10 }11 template void f(T &&t) {12 g(t);13 }14 int main()15 {16 f(42);17 return 0;18 }
编译器会报错,因为传给g的是名为t的参数。函数参数与其他任何变量一样,都是左值表达式。而g接受的是一个右值表达式。
2)在调用中使用std::forward保持类型信息
forward定义在头文件utility中,forward必须通过显式模板实参来调用。通常情况下,我们使用forward来传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性。
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 9 void g(int &&i) {10 std::cout << __FUNCTION__ << ", " << i << std::endl;11 }12 template void f(T &&t) {13 g(std::forward (t));14 }15 int main()16 {17 f(42);18 return 0;19 }
如果实参是一个右值,则T是一个普通(非引用)类型,forward<T>将返回T&&。如果实参是一个左值,则通过引用折叠,T本身是一个左值引用类型。在此情况下,返回类型是一个指向左值引用类型的右值引用。再次对forward<T>的返回类型进行引用折叠,将返回一个左值引用类型。