按照之前的进度我们已经讲了模板三种参数类型,那么本应该乘胜追击的来讲讲变长模板,但是我想把这个议题讲的更深刻一点,为啥呢,因为作为C++11最闪耀的一个新特性,它太重要了,而模板从变长模板加入以后这个学习的路线突然给打乱了,想要讲解变长模板需要其他的知识,而其他的知识又穿插了变长模板的内容,所以干脆我暂且不考虑变长模板的情形,专心得讲解原先C98老标准中的知识,等最后再来讲解它。好,我们看看今天的内容,函数模板。
之前的两篇文都着重在写类模板的知识,其实相对而言类模板是比较简单的,它的使用在实际项目中虽然更多更广泛,但模板真正繁杂的部分几乎都出自于函数模板,但在实际项目中见到的少绝不代表他不重要,以我自己的理解,在大多数的模板工具库中,函数模板真正起到了承上启下,画龙点睛的作用,好了,我们先从简单的来看看。
首先应该明白的是,模板包含了类模板和函数模板,类模板我们之前已经讲过了,剩下的内容几乎都是函数模板了,所以按照后续的章节估算来看,函数模板的内容显然是多于类模板的,其实类模板非常简单,不过就是几种形式,把模板参数套用进去就好了,而函数模板就远不止于此了,包含的议题非常之多。
我们先来看个例子:
template<typename T, int N>
void function()
{}
这是一个看起来非常简单的模板函数,但是不得不说很多人理解不了,我听过太多的人问我这个函数定义里,T和N两个参数都没有在函数中实际使用,为什么这两个参数还能存在,首先,谁告诉你的模板参数一定要和定义有关系?这个问题换句话说,模板参数到底定义了什么?无论对于类模板还是函数模板来讲,这个答案简单来说,不同的模板参数对应了不同的模板实例,而模板本身是否使用这些模板参数这完全是需求上的问题了,可是对于模板来讲,如果没有在定义中用到模板参数那么每个版本的定义岂不是没有差异了,是的,没有差异,但是他们是不同的实例,而且实例间毫无关系,这点一定要搞清楚,否则很多概念会混乱不堪。
那么,在使用上我们之前讲了模板的实例化,一个模板自身是不可以被直接使用的,一定要在被实例化后才能生成一个可用的实例,同样对于函数模板而言也存在实例话,不仅如此还同样存在显式实例化和隐式实例化,我们先来看看函数的显式实例化:
在此多说两句,大家对实例化和特化的格式一定要熟悉,至少要一眼看出来是特化还是实例化才行,表面上看去很简单,其实有些情况不是很好分别,特别是阅读一些本来就很难读懂的代码正头大的时候就很容易出错,还有一些生僻的用法不是很常见,更容易出错,例如下面例子,得能不用想一眼清才行:
template int PrimaryDef<char>::sMem;
template<>
int PrimaryDef<int>::sMem = 10000;
好,发散了一下,回到正题,再看看函数模板的实例化,对于上面的function函数的实例化并没有上面特别之处按照格式套入即可,
template void function<int, 10>();
对于上面的例子,这种实例化方法已经足够了,但是对于一些特殊的函数模板还存在另一种实例化的方式,比如:
template <typename T>
void func(T arg)
{}
对于上面这个函数来讲,我们可以通过指定参数来进行实例化:
template void func(int);
template void func<>(int);
这种方式虽然比较少见,但如果遇到一定要认识,否则就悲剧了,对于这个函数模板,其模板参数T在函数中给函数参数做了类型,所以只要函数类型确定了,模板参数也就可以确定了,所以同样能实例化。这个小括号要和不要都可以。
对于函数的特化就不再啰嗦了,没有太大意义,跟类模板属于一个套路,唯一不同的而且要强调的是,函数模板不支持偏特化,如果在使用上遇到这样的情况那么要通过函数重载来解决。关于这点我们后面再说,现在首先要介绍的是对于函数模板一个非常重要的议题,叫做函数模板的实参演绎(Template arguments deduction)。
首先介绍一下什么叫实参演绎,不敢胡诌,所以把cppreference上面的一段搬出来:
In order to instantiate a function template, every template argument must be known, but not every template argument has to be specified. When possible, the compiler will deduce the missing template arguments from the function arguments. This occurs when a function call is attempted, when an address of a function template is taken, and in some other contexts.
还是大致介绍一下,意思是说当实例化一个函数模板时,必须清楚每一个模板参数,但是并不是说必须要显示指定,如果编译器可以从传递的实参推导出来模板参数的也同样可以,那么实参演绎通常发生在函数调用或者对函数模板取地址赋值的时候,还有一些特殊情况,后面再说。
我们先来看个例子,为了提高效率代码不再指出格式细节,比如不再强调函数定义格式等等,我尽量从cppreference上取原汁原味的例子:
template<typename To, typename From> To convert(From f);
void g(double d)
{
int i = convert<int>(d); // calls convert<int, double>(double)
char c = convert<char>(d); // calls convert<char, double>(double)
int(*ptr)(float) = convert; // instantiates convert<int, float>(float)
}
我们可以看到convert函数有两个模板参数,To和From,那么指定了其中的一个,对于这种情况,有其基本原则,指定的模板参数按照模板参数列表从左往右的顺序来指定,没有被指定的模板参数会根据函数的实参来演绎,注意,除非你想使用SFINAE的特性,否则这里指定的模板参数跟函数参数类型一定不能有前后不一的情况,这点我们后续再来讨论。所以按照规则,例子中前两处对convert的使用分别是通过指定了模板参数To,而From则从传入的参数来推导,以为实参d的类型为double,所以From很自然的被推断为double类型,而最后一个比较特殊,对ptr指针赋值,其类型为int (*)(float),将convert赋值给ptr时会通过其类型的返回值和参数共同推导出To和From,注意,这里比较特殊,连返回值类型都用上了,而函数调用涉及的实参演绎返回值类型是没有资格参加的。
编译器根据调用方传入的实参类型Ai形成与形参Pi之间的配对,从而确定每一模板参数(在此设定模板类型参数为Ti,模板模板参数为TTi,非类型模板参数Ii),其中的i表示多个参数存在时的下标。那么在匹配时,每一个Ai和Pi都会形成一对,然后根据每一对来分别做推导,如果推导出来的结果间有矛盾会出现匹配失败的情况。在这个时期是不存在隐式转型的。比如:
#include <iostream>
template<typename T>
void func(T p1, T p2)
{}
int main()
{
func(1, 2.5);
}
在这个例子中func的参数p1,p2都是T类型的,但是在调用方分别使用了int和double类型的参数,套用上述的规则P1 = T ,A1=1, T = int, P2 = T ,A2 = 2.5, T = double,这两对分别推导出不同的T所以会发生匹配失败的情况,编译信息如下
Test.cpp:4:6: note: template argument deduction/substitution failed:
Test.cpp:9:16: note: deduced conflicting types for parameter ‘T’ (‘int’ and ‘double’)
不过需要注意的是我在上面所用的词是匹配失败,但强调一点匹配失败并不是一个错误,可能有人会比较疑惑为什么不是错误还是报错了?这是因为我们没能为func的调用找到一个匹配成功的情况,换句话说,如果存在一个func定义能满足func(1, 2.5)这样,那么就不会报错了,这点其实就是著名的SFINAE的最基本意思。我们留在以后有机会讨论。
我想对模板参数演绎已经有一个基本的了解了,那现在看看这个语法的细节部分。
首先,有些特殊的情况要介绍一下,对于函数的模板是一个函数指针类型时,如果传入的实参是一组重载函数,那么如果仅有一个函数可以最佳匹配参数类型的时候就会以此函数版本作为参数调用,否则会报错,这点只需要看个例子就好,不用太在意。
template<class T> int f(T(*p)(T));
int g(int);
int g(char);
f(g); // P = T(*)(T), A = overload set
// P = T(*)(T), A1 = int(int): deduced T = int
// P = T(*)(T), A2 = int(char): fails to deduce T
// only one overload works, deduction succeeds
在模板参数演绎以前,编译器通常会做一些工作来让匹配更容易,主要如下:
- 如果P不是一个引用类型
template<class T> void f(T);
int a[3];
f(a); // P = T, A = int[3], adjusted to int*: deduced T = int*
const int b = 13;
f(b); // P = T, A = const int, adjusted to int: deduced T = int
void g(int);
f(g); // P = T, A = void(int), adjusted to void(*)(int): deduced T = void(*)(int)
- 如果P是一个CV限定类型,则忽略其CV属性
- 如果P是一个引用类型,那么P将被修正为所引用的类型
- 如果P是一个非cv限定的右值引用,而A是一个左值,那么A将被修正为该左值类型的左值引用(C++11)
template<class T>
int f(T&&); // P is an rvalue reference to cv-unqualified T (forwarding reference)
template<class T>
int g(const T&&); // P is an rvalue reference to cv-qualified T (not special)
int main()
{
int i;
int n1 = f(i); // argument is lvalue: calls f<int&>(int&) (special case)
int n2 = f(0); // argument is not lvalue: calls f<int>(int&&)
// int n3 = g(i); // error: deduces to g<int>(const int&&), which
// cannot bind an rvalue reference to an lvalue
}
f是一个非cv的右值引用类型,而实参i是一个左值,所以按照上面的规则会被调整为左值引用,所以T=int&,而f的形参类型int&&&会产生引用折叠为一个左值引用。多说一句,什么是左值什么是右值,简单来说能取地址的就是左值,不能取地址就是右值,而不是说等号左右的区别。
- 推导后的类型可以比实参类型A多顶层的cv限定,举个下面的例子:
template<typename T> void f(const T& t);
bool a = false;
f(a); // P = const T&, adjusted to const T, A = bool:
// deduced T = bool, deduced A = const bool
// deduced A is more cv-qualified than A
首先,因为f的P是一个引用类型,所以去掉引用,P=const T,而A = bool,那么T只能是bool,而代入后类型为const bool比A多一个const,这在C++中是允许的。
- 如果传递的实参类型可以通过限定转型(qualifiers conversion)转换成推导后的类型,则是允许的,还是看个例子
template<typename T> void f(const T*);
int* p;
f(p); // P = const T*, A = int*:
// deduced T = int, deduced A = const int*
// qualification conversion applies (from int* to const int*)
是不是觉得跟上面的例子没区别?实际上是有很大区别的,这例子里的f形参类型是指针类型,而const的位置在*之前,所以,这个指针类型不是const的。那么限定转型可以将int*转换成const int*,所以可以编译通过。
- 如果P是一个模板类型的引用或直接,而传递的A是P的子类模板类型,那么同样可以,说起来有点绕,还是看个例子来说明。
template<class T> struct B { };
template<class T> struct D : public B<T> { };
template<class T> void f(B<T>&) { }
void f()
{
D<int> d;
f(d); // P = B<T>&, adjusted to P = B<T> (a simple-template-id), A = D<int>:
// deduced T = int, deduced A = B<int>
// A is derived from deduced A
}
根据之前的描述,f的参数为引用类型,转变为P=B<T>,而A为D<int>,推导出T=int后,P为B<int>而A是其子类。是不是觉得巨恶心巨无趣,没错就是这样。
- 如果P是一个嵌套名说明符(nested-name-specifier),那么其所含的模板参数是不可推导的。首先要说的是啥叫嵌套名称说明符,其实就是用来限定作用域的::符号的左边那部分。比如std::out中std就是嵌套名说明符。那么这里所特指的主要是内中嵌套内部类的情况。不举个栗子肯定不行:
// the identity template, often used to exclude specific arguments from deduction
template<typename T> struct identity { typedef T type; };
template<typename T> void bad(std::vector<T> x, T value = 1);
template<typename T> void good(std::vector<T> x, typename identity<T>::type value = 1);
std::vector<std::complex<double>> x;
bad(x, 1.2); // P1 = std::vector<T>, A1 = std::vector<std::complex<double>>
// P1/A1: deduced T = std::complex<double>
// P2 = T, A2 = double
// P2/A2: deduced T = double
// error: deduction fails, T is ambiguous
good(x, 1.2); // P1 = std::vector<T>, A1 = std::vector<std::complex<double>>
// P1/A1: deduced T = std::complex<double>
// P2 = identity<T>::type, A2 = double
// P2/A2: uses T deduced by P1/A1 because T is to the left of :: in P2
// OK: T = std::complex<double>
这个例子也不知道是哪个大神想出来的,太经典以至于我第一次居然没看懂,简单解释一下:
- 如果非类型模板参数存在于子表达式中,则模板参数不可推导,这个比较简单,举例说明就可以了
template<std::size_t N> void f(std::array<int, 2 * N> a);
std::array<int, 10> a;
f(a); // P = std::array<int, 2 * N>, A = std::array<int, 10>:
// 2 * N is non-deduced context, N cannot be deduced
// note: f(std::array<int, N> a) would be able to deduce N
- 函数的默认参数不能用来做推导T
template<typename T, typename F>
void f(const std::vector<T>& v, const F& comp = std::less<T>());
std::vector<std::string> v(3);
f(v); // P1 = const std::vector<T>&, A1 = std::vector<std::string> lvalue
// P1/A1 deduced T = std::string
// P2 = const F&, A2 = std::less<std::string> rvalue
// P2 is non-deduced context for F (template parameter) used in the
// parameter type (const F&) of the function parameter comp,
// that has a default argument that is being used in the call f(v)
- 如果A是一组重载函数,且存在多于一个的匹配或不存在匹配时,则T不可推导。
template<typename T> void out(const T& value) { std::cout << value; }
out("123"); // P = const T&, A = const char[4] lvalue: deduced T = char[4]
out(std::endl); // P = const T&, A = function template: T is in non-deduced context
- 如果A是一个初始化列表,而P不是std::initializer_list类型或其引用类型,则P中的T不可推导
#include <vector>
#include <initializer_list>
template<class T> void g1(std::vector<T>);
template<class T> void g2(std::vector<T>, T x);
template<class T> void g3(std::initializer_list<T>);
g1({1, 2, 3}); // P = std::vector<T>, A = {1, 2, 3}: T is in non-deduced context
// error: T is not explicitly specified or deduced from another P/A
g2({1, 2, 3}, 10); // P1 = std::vector<T,> A1 = {1, 2, 3}: T is in non-deduced context
// P2 = T, A2 = int: deduced T = int
g3({1, 2, 3}) // P = std::initializer_list<T>, A = {1, 2, 3}: deduced T=int
- 如果P中包含一个模板参数列表,且有参数扩展包(pack expansion C++11),如果其位置不在P的模板参数列表的最后,则不可推导。
template<int...> struct T { };
template<int... Ts1, int N, int... Ts2>
void good(const T<N, Ts1...>& arg1, const T<N, Ts2...>&);
template<int... Ts1, int N, int... Ts2>
void bad(const T<Ts1..., N>& arg1, const T<Ts2..., N>&);
T<1, 2> t1;
T<1, -1, 0> t2;
good(t1, t2); // P1 = const T<N, Ts1...>&, A1 = T<1, 2>:
// deduced N = 1, deduced Ts1 = [2]
// P2 = const T<N, Ts2...>&, A2 = T<1, -1, 0>:
// deduced N = 1, deduced Ts2 = [-1, 0]
bad(t1, t2); // P1 = const T<Ts1..., N>&, A1 = T<1, 2>:
// <Ts1..., N> is non-deduced context
// P2 = const T<Ts2..., N>&, A2 = T<1, -1, 0>:
// <Ts2..., N> is non-deduced context
- 如果P是一个非引用数组类型,则其主边界不可推导。
template<int i> void f1(int a[10][i]);
template<int i> void f2(int a[i][20]); // P = int[i][20], array type
template<int i> void f3(int (&a)[i][20]); // P = int(&)[i][20], reference to array
void g()
{
int a[10][20];
f1(a); // OK: deduced i = 20
f1<20>(a); // OK
f2(a); // error: i is non-deduced context
f2<10>(a); // OK
f3(a); // OK: deduced i = 10
f3<10>(a); // OK
}
#include <iostream>
using namespace std;
template<typename T> class TEMP {};
struct A
{class In{};};
struct B{};
template<template<typename> class TT, typename T>
void func(TT<T> t, typename T::In*)
{ cout << "#1\n"; }
template<template<typename> class TT, typename T>
void func(TT<T> t, ...)
{ cout << "#2\n"; }
template<template<typename> class TT, typename T>
void forwarding(TT<T> a)
{ func(a, nullptr); }
int main() {
TEMP<A> ta;
TEMP<B> tb;
forwarding(ta);
forwarding(tb);
}
T
; (P的类型为T)
cv-list T
; (P的类型为带const或volatile的T)
T*
;(P的类型为T的指针类型)
T&
;(P的类型为T的引用类型)
T&&
;(P的类型为T的右值引用类型,C++11)
T[integer-constant]
;(P的类型为T的数组类型,其中integer-constant为已知的常数,注意这里不是引用类型,如果把integer-constant改成一个非类型参数N,那么N是不能不推导出来的。但是引用类型可以,见后述)
- T (&)[N];(P的类型为T类型长度为N的数组引用类型,这个类型可能很多人不认识,比如说int a[5],那么它的类型为int[5]但是其引用类型为int (&)[N],指针类型为int (*)[N],如果要定义一个变量其格式为int (&p)[N],p的类型就是int[5]的引用类型。)
class-template-name<T>
;(P为一个模板类以模板参数T为实例化的类型的参数)
type(T)
;(这个类型估计见过的也不多,是返回值为type参数为T的函数类型,注意,不是函数指针类型)
T()
;(同上函数类型)
T(T)
;(同上函数类型)
T type::*
;(这个类型估计见过的更少,是类的成员非static成员指针类型,其自身类型为T,是type的一个成员,此处type已知,注意这里只是说起成员类型,并不特指成员指针和成员函数,这两者都包含其中,例如type中有一个成员变量a,和一个成员方法func,那么在传递参数A的时候,可以传递&type::a和&type::func,有人说type是一个类型,不是一个实例,怎么能通过type使用a,嗯,确实可以,不明白的话google)
type T::*
;(同上,只是此处其自身类型已知,所属的类未知,有人说不是嵌套名说明符不可推导吗,嗯,你这么想也对,不过这里和上面有明显的差别,上面的情况是嵌套类,而这里不是,是其成员)
T T::*
;(同上,类型和所属类都不知。这里并不要求两个T是相同的,也就是说可以是T1 T2::*)
T(type::*)()
;(P是type类的成员函数,其自身类型为T())
type(T::*)()
;(同上成员函数)
type(type::*)(T)
;(同上成员函数)
type(T::*)(T)
;(同上成员函数)
T (type::*)(T)
;(同上成员函数)
T (T::*)()
;(同上成员函数)
T (T::*)(T)
;(同上成员函数,忘了说明一下,如果要声明一个具体参数p,格式为T (T::*p)(T),其中的三个T可以分别替换为T1,T2,T3,均可推导)
type[i]
;(数组类型)
class-template-name<I>
;(模板类型,只是其参数为非类型模板参数I)
TT<T>
;(模板模板参数,TT和T均可推导)
TT<I>
;(模板模板参数,I为非类型模板参数,TT和I均可推导)
TT<>
;(模板模板参数,这个比较特殊是匹配C++11中可变模板参数的。)
#include <iostream>
using namespace std;
struct A
{
void func(int){}
int a;
/* data */
};
template<typename...T>
class TA
{};
template<typename T1, typename T2, typename T3>
void func1(T1 (T2::*p)(T3))
{}
template<typename T1, typename T2>
void func2(T1 T2::*p)
{}
template<typename T1, int I>
void func3(T1 (&p)[I])
{}
template<typename T1, int I>
void func4(T1 (*p)[I])
{}
template<template<typename...> class TT>
void func5(TT<> p)
{}
int main() {
int a[5];
TA<> ta;
func1(&A::func);
func2(&A::func);
func2(&A::a);
func3(a);
func4(&a);
func5(ta);
}