S16模板与泛型编程
一、定义模板
1、函数模板
定义一个通用的函数模板用来生成针对特定类型的函数版本,模板定义以关键字template
开始,后跟一个以逗号分隔的一个或多个模版参数的模板参数列表,当使用模板时显式或隐式地指定模板实参并将其绑定到模板参数上
template <typename T>
int compare(const T &v1, const T &v2) {...}
(1)实例化函数模板
当调用一个函数模板时,编译器(通常)用函数实参来推断模板实参,称为实例化,不同的函数实参生成不同的函数版本,这些版本称为模板的实例
(2)模板类型参数
类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换
template <typename T> T foo(T* p) //注意,template<..>中每个参数前都要有类型参数
{
T tmp = *p;
...
return tmp;
}
注意:类型参数typename
和class
没有差别,如template<typename T, class U>
,但更推荐用typename
(3)模板非类型参数
一个非类型参数表示一个值而非一个类型,通过一个特定的类型名而非typename/class
来指定非类型参数,当一个模板实例化时,非类型参数必须被一个常量表达式替换
注意:绑定到非类型整型参数必须是常量表达式,绑定到指针或引用的实参必须具有静态的生存期(static
的变量其地址不会改变因此其指针值也可以认为是常量表达式,而局部变量或动态对象可能会变化),而指针也可以用nullptr/0
来实例化
template <unsigned N, unsigned M> int compare(const char (&p1)[N], const char (&p2)[M]);
(4)inline
和constexpr
的函数模板
函数模板也可以声明为inline/constexpr
的,说明符放置在模板参数列表之后,返回类型之前
template <typename T> inline T min(const T&, const T&);
(5)编写类型无关的代码
模板程序应该尽可能减少对实参类型的要求
(6)模板编译
当编译器遇到一个模板定义时并不生成代码,只有当实例化出模板的一个特定版本时,编译器才会生成代码,为了实例化,编译器需要掌握模板的定义,因此与非模板代码(头文件中声明,非头文件中定义)不同,模板的头文件中既包含声明也包含定义
(7)大多数编译错误在实例化期间报告
保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作
2、类模板
(1)定义类模板
与函数模板不同,编译器不能为类模板推断模板参数类型,同样以template
开始后跟模板参数列表
(2)实例化类模板
使用类模板时,必须提供显式模板实参列表,被绑定到模板参数,编译器从模板实例化出一个类时会重写模板并将参数的每个实例替换为给定的模板实参
注意:一个类模板的每个实例都形成一个独立的类,相互之间没有任何关联,相互也不会有任何特殊访问权限
(3)在模板作用域中引用模板类型
在一个模板中可以再使用另一个模板,并且直接提供外层模板的模板参数即可,在实例化时会正常运行
(4)类模板的成员函数
类模板的成员函数与普通类的成员函数定义一样,可以在类内类外,类内时自动inline
,但是在类外时,需要以template
开头并且有模板参数列表,其他与普通类的定义一致
template <typename T> //成员函数以template开头
void Blob<T>::pop_back(); //同样要有作用域说明,且是模板类和模板参数列表Blob<T>
(5)类模板成员函数的实例化
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化,使得即使某种类型不能完全符合模板操作的要求,也能用该类型实例化类
(6)在类代码内简化模板类名的使用
当在模板类自己的作用域内时,可以直接使用模板名而不提供实参,其余情况都要提供实参
template <typename T> class BlobPtr
{
...
BlobPtr& operator++(); //并不是BlobPtr<T>& operator++();省略的<T>由编译器自动处理
}
(7)在类模板外使用类模板名
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
BlobPtr ret = *this; //保存当前值,此时已进入类模板作用域,因此BlobPtr不需要<T>
++*this; //前置++会检查是否合法
return ret;
}
注意:在一个类模板的作用域内,可以直接使用模板名而不必指定模板实参
(8)类模板和友元
如果一个类模板包含一个非模板友元,则友元被授权为所有模板实例;如果友元自身是模板则类可以授权给所有友元模板实例也可以只授权给特定实例
(9)一对一友好关系
类模板与另外一个(类或函数)模板间友好关系的最常见形式是建立对应实例及其友元间的友好关系
template <typename> class BlobPtr; //前置声明,在Blob中声明友元所需要的
template <typename> class Blob; //operator==中的参数所需要的前置声明
template <typename T> bool operator==(const Blob<T>&, const Blob<T>&);
template<typename T> class Blob
{
//每个Blob实例将访问权限授予用相同类型T实例化的BlobPtr和相等运算符
friend class BlobPtr<T>;
friend bool operator==<T> (const Blob<T>&, const Blob<T>&);
...
}
(10)通用和特定的模板友好关系
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定实例为友元
template <typename T> class Pal; //前置声明,在将模板的一个特定实例声明为友元时要用到
class C //C是普通的非模板类
{
friend class Pal<C>; //用类C实例化的Pal是C的一个友元
template <typename T> friend class Pal2; //Pal2的所有实例都是C的友元,这种情况无须前置声明
};
template <typename T> class C2 //C2是模板类
{
friend class Pal<T>; //一对一友好,C2的每个实例都将相同T实例化的Pal声明为友元
//且Pal的模板声明必须在作用域内(即需要前置声明)
template <typename X> friend class Pal2; //Pal2的所有实例都是C2每个实例的友元,不需要前置声明
friend class Pal3; //Pal3是非模板类,是C2所有实例的友元
};
注意:声明特定友元时需要前置声明,将所有实例声明成友元时需要使用与类模板本身不同的模板参数
(11)令模板自己的类型参数成为友元
新标准中允许将模板类型参数声明为友元,例如某个类型名Foo
,Foo
将成为Bar<Foo>
的友元
template <typename Type> class Bar
{
friend Type;
...
}
(12)模板类型别名
一个模板类型别名是一族类的别名,且可以固定一个或多个模板参数
template <typename T> using twin = pair<T, T>;
twin<string> authors; //authors是一个pair<string, string>
template <typename T> using twin2 = pair<T, unsigned>;
twin2<string> books; //books是一个pair<string, unsigned>
(13)类模板的static
成员
与任何其他static
数据成员相同,模板类的每个static
数据成员必须有且仅有一个定义,但是类模板的每个实例都有一个独有的static
对象,将static
数据成员也定义成模板,同样的只有在使用时才会实例化一个static
成员
template <typename T> class Foo
{
public:
static size_t count() { return ctr; }
private:
static size_t ctr;
}
//对任意X,都有一个Foo<X>::ctr和一个Foo<X>::count,所有Foo<X>类型的对象共享
template <typename T>
size_t Foo<T>::ctr = 0; //类外对static数据成员也定义为模板,这样对每个X,所有Foo<X>共享一个静态数据成员
Foo<int> fi; //实例化Foo<int>类和static数据成员ctr
auto ct = Foo<int>::count(); //实例化Foo<int>::count,类模板、模板类成员函数使用到的时候才实例化
ct = fi.count(); //使用Foo<int>::count
ct = Foo::count(); //错误,使用哪个模板实例的count不明
3、模板参数
(1)模板参数与作用域
模板参数遵循普通的作用域规则,一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前,且模板参数会隐藏外层作用域中声明的相同名字,但在模板内不能重用模板参数名
(2)模板声明
与函数参数相同,声明中模板参数的名字不必与定义中相同,但必须有相同数量和种类(类型/非类型)的参数
(3)使用类的类型成员
在普通(非模板)代码中,编译器掌握类的定义,因此当使用::
时明确知道要访问的名字是类型还是static
成员;但是模板不可以,直到实例化之前编译器并不知道::
后的名字是类型还是static
成员,因此当希望使用的是类型时需要使用typename
显式说明这个名字是类型
template <typename T>
typename T::value_type top(const T& c); //编译器不知道T::value_type是类型还是static成员,typename显式说明
(4)默认模板实参
template <typename T, typename F = less<T>> //默认模板实参less<T>
int compare(const T &v1, const T &v2, F f = F()) //默认函数实参F
{
if(f(v1,v2)) return -1;
if(f(v2,v1)) return 1;
return 0;
}
(5)模板默认实参与类模板
无论何时使用类模板时,名字后都要有尖括号,当使用了默认模板参数时也要加上空的尖括号<>
template <typename T = int> class Numbers {...}
Numbers<long double> lots_of_precision;
Numbers<> average_int; //空<>表示使用了默认int
4、成员模板
一个类(普通类或类模板)可以包含本身是模板的成员函数,这种成员称为成员模板,且不能是虚函数
(1)普通(非模板)类的成员模板
(2)类模板的成员模板
类模板和成员模板各自独立模板参数,在类模板外定义成员模板时,需要先提供类模板参数列表,再提供成员模板参数列表
template <typename T>
template <typename It>
Blob<T>::Blob(It b, It e): ... {...}
(3)实例化与成员模板
为了实例化一个类模板的成员模板,必须同时提供类和函数模板的实参
template<typename T>
template<typename It>
Blob<T>::Blob(It b, It e) : data(make_shared<vector<T>>(b, e)) {}
int ia[] = { 0,1,2,3 };
vector<long> vi({ 0,1,2,3,4 });
list<const char*> w({ "now", "is", "the", "time" });
Blob<int> a1(begin(ia), end(ia));
Blob<double> a2(vi.begin(), vi.end());
Blob<string> a3(w.cbegin(), w.cend());
5、控制实例化
(1)显式实例化
在多个文件中实例化相同的模板会带来不必要的额外开销,可以通过显式实例化只实例化相同模板的一个实例
extern template ...; //实例化声明,实际实例化位置在其他文件中,避免在本文件又重复实例化
template ...; //实例化定义,显式进行实例化
extern template class Blob<string>;
template int compare(const int&, const int&);
当编译器遇到extern
模板声明时不会在本文件中生成实例化代码,对于一个给定的实例化版本可以有多个extern
声明而只能有一个定义,并且extern
声明必须出现在任何使用此实例化版本的代码之前
注意:对每个实例化声明,在程序中的某个位置必须有其显式的实例化定义
(2)实例化定义会实例化所有成员
与处理类模板的普通实例化(用到哪个成员才实例化哪个成员)不同,类模板的实例化定义会实例化该模板的所有成员,因此当显式实例化某个类模板时,必须能用于模板的所有成员
6、效率与灵活性
以shared_ptr/unique_ptr
为例,参考《C++ primer》p.599,通过在编译时绑定删除器,unique_ptr
避免了间接调用删除器的运行时开销;通过在运行时绑定删除器,shared_ptr
使用户重载删除器更为方便
二、模板实参推断
1、类型转换与模板类型参数
(1)类型转换
编译器通常不对模板实参进行类型转换,而是生成一个新的模板实例,只有很有限的类型转换会自动执行
- 顶层
const
无论在形参还是实参中都会被忽略,与常规函数调用一样 const
转换,可以将一个非const
对象的引用/指针传递给一个const
的引用/指针形参- 数组/函数指针转换,如果函数形参不是引用类型,则可以对数组/函数类型的实参应用正常的指针转换,即一个数组可以转换为指向首元素的指针,一个函数可以转换为一个该函数类型的指针
注意:算术类型、派生类向基类、用户重载的转换等都不能应用于函数模板
template <typename T> T fobj(T, T); //实参被拷贝
template <typename T> T fref(const T&, const T&); //引用
string s1("abc");
const string s2("abc");
fobj(s1, s2); //调用fobj(string, string),s2的const被忽略
fref(s1, s2); //调用fref(const string &, const string &),s1转换为const
int a[10], b[42];
fobj(a, b); //调用fobj(int *, int *); 执行数组到首元素指针的转换
fref(a, b); //错误,引用时不转换,且传入数组引用时a,b维度不同则类型不同,类型不匹配
(2)使用相同模板参数类型的函数形参
一个模板类型参数可以用作多个函数形参的类型,但由于转换的限制,传递给这些形参的实参必须具有相同的类型,否则调用是错误的
(3)正常类型转换应用于普通函数实参
函数模板并不一定所有形参都是模板类型参数,可以是普通类型的,此时按一般函数调用的转换规则
template <typename T> ostream &print(ostream &os, const T &obj);
2、函数模板显式实参
(1)指定显式模板实参
与定义类模板实例的方式相同,显式模板实参在尖括号中给出,位于函数名之后和实参列表之前,并且按顺序与对应的模板参数匹配
template <typename T1, typename T2, typename T3> T1 sum(T2, T3);
auto val = sum<long long>(10, 20); //long long按顺序给T1和T2,此时T3未给出则根据20自动推断为T3为int
auto val = sum<long, long, long>(10, 20); //T1、T2、T3都显式指定为long
注意:返回类型(如上的T1)必须显式给出,形参可以根据实参推断
(2)正常类型转换应用于显式指定的实参
注意:对于模板类型参数已经显示指定的函数实参,也按照一般规则可以进行正常的类型转换
3、尾置返回类型与类型转换
(1)尾置返回类型
模板函数可以通过尾置返回类型结合deltype
而不是显式给定来让编译器自己推导返回类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg) //尾置返回类型需要原返回类型所在的位置为auto
{
return *beg; //返回一个*beg的引用
}
(2)进行类型转换的标准库模板类
标准库的类型转换模板定义在头文件type_traits
中,参见《C++ primer》p.606
template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
return *beg; //由于使用了类型转换模板remove_reference,将返回*beg的拷贝而不是引用
}
4、函数指针和实参推断
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值
template <typename T> int compare(const T&, const T&);
int (*pf1)(const int &, const int &) = compare; //pf1指向int compare(const int &, const int &)
void func(int(*)(const string &, const string &));
void func(int(*)(const int&, const int&));
func(compare); //错误,二义性,两个重载函数都是最佳匹配
func(compare<int>); //正确,传递compare(const int&, const int&)
5、模板实参推断和引用
(1)从左值引用函数参数推断类型
(2)从右值引用函数参数推断类型
(3)引用折叠和右值引用参数
将左值传递给一个接受右值引用参数的函数是不合法的,但是在类型别名/模板参数下定义了2个例外
- 当一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数(如
T&&
)时,编译器推断模板类型参数为实参的左值引用类型(即推断T
是X&
) - 如果间接创建一个引用的引用,则这些引用会形成折叠,折叠规则为:
X& &
X& &&
X&& &
都折叠为X&
,而X&& &&
折叠为X&&
template <typename T> void f3(T &&); //T &&
f3(42); //T = int
f3(i); //T = int &, 则f3(int & &&)折叠为f3(int &)
注意:这就会使得如果一个函数参数是指向模板参数类型的右值引用,则可以传递给它任意类型的实参,当传递一个左值时则函数参数被实例化为一个普通的左值引用
(4)编写接受右值引用参数的模板函数
由于引用折叠的存在,当代码中涉及的类型可能是普通非引用也可能是引用类型时,会带来诸多问题,实际中右值引用通常用于两种情况:模板转发其实参、模板被重载
template <typename T> void f(T &&); //绑定非const 右值
template <typename T> void f(const T&); //绑定左值和const右值
6、理解std::move
(1)std::move
是如何定义的
template <typename T>
typename remove_reference<T>::type &&move(T &&t) //T &&模板参数使得可以传给move任何类型
{
//T为X则X&&,并通过remove_reference什么也不做,同时加上&&,X&& &&折叠为X&&,最终传入的X&&依然是X&&右值引用
//T为X&则X& &&折叠为X&,并通过remove_reference变为X,同时加上&&,最终传入的X&变为了X&&右值引用
//T为X&&则X&& &&折叠为X&&,并通过remove_reference变为X,同时加上&&,最终传入的X&&依然是X&&右值引用
return static_cast<typename remove_reference<T>::type &&>(t);
}
(2)std::move
是如何工作的
string s1("hi"), s2;
s2 = std::move(string("bye")); //接受右值,T是string而t是string&&,则remove_reference的type是string
//对应static<string &&>(t),则什么也不做,最后返回string &&
s2 = std::move(s1); //move接受一个左值,T是string &而t是string &(引用折叠)
//则remove_reference的type是去掉&的string
//对应static_cast<string &&>(t),t就被转换为string &&
//最终两者都完成了右值绑定到左值,而s1此时内容是不确定的(move到s2了)
(3)从一个左值static_cast
到一个右值引用是允许的
通常static_cast
只能用于其他合法的类型转换,但是可以显示地将一个左值转换为一个右值引用(隐式不转换)
7、转发
(1)定义能保持类型信息的函数参数
通过将一个函数参数定义为一个指向模板类型参数的右值引用,可以保持其对应实参的所有类型信息,如const
属性、左/右值属性
(2)在调用中使用std::forward
保持类型信息
move/forward
都定义在头文件utility中,forward
必须通过显式模板实参来调用,返回该显式实参类型的右值引用,即forward<T>
返回T&&
template <typename F, typename T1, typename T2>
void flip3(F f, T1 &&t1, T2 &&t2) //使用T &&保持了传进来参数的属性
{
f(std::forward<T1>(t1), std::forward<T2>(t2)); //使用std::forward<T>()保持了再进一步调用参数的属性
}
即利用T &&
和forward<>()
可以实现完美转发,保留参数的属性将参数转发给其他函数
注意:通过对一个指向模板参数类型的右值引用函数参数(T&&
),使用forward
会保持实参类型的所有细节
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t1, t2);
}
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t1, t2);
}
template <typename F, typename T1, typename T2>
void flip3(F f, T1 &&t1, T2 &&t2) //保留t1/t2的所有细节
{
f(std::forward<T1>(t1), std::forward<T2>(t2)); //进一步保留t1/t2的所有细节
}
三、重载与模板
涉及函数模板时,函数匹配规则会有以下变化:
- 对于一个调用,其候选函数包括所有可行的模板实参推断成功的函数模板实例
- 候选函数(模板与非模板)按类型转换排序(对于模板,类型转换很有限)
- 选择最佳匹配函数,若有多个同样好的匹配,则①只有一个非模板函数则选择之②某个模板函数更特例化则选择之③二义性错误
1、编写重载模板
template <typename T> string debug_rep(const T &t); //[1]
template <typename T> string debug_rep(T *p); //[2]
string s("hi");
debug_rep(s); //[1]为const string &能符合,非指针故[2]不可行,调用[1]
debug_rep(&s); //[1]为const string *&,[2]为string *,[1]需要到const的转换,[2]精确匹配,调用[2]
2、多个可行模板
const string *sp = &s;
debug_rep(sp); //[1]为const string *&,[2]为const string *
//由于[1]能匹配任何参数,[2]只能匹配指针,[2]更特例化,故根据重载模板特殊规则,调用[2]
3、非模板与模板重载
string debug_rep(const string &s); //[3]
debug_rep(s); //[1]为const string &能符合,非指针故[2]不可行,[3]为const string &能符合
//[3]非模板函数最特例化,故根据最特例化规则,非模板显然比模板特例,调用[3]
4、重载模板和类型转换
debug_rep("hi"); //[1]为const char &[10],[2]为const char *,[3]为const string &
//[3]要求从const char*->string类型转换,因此[1][2]精确匹配,且[2]更特例化,调用[2]
5、缺少声明可能导致程序行为异常
注意:在定义任何函数之前,需要声明所有重载的函数版本,否则可能会出现由于编译器未遇到希望调用的函数而实例化了一个不希望调用的函数版本,但也不会报错的问题
四、可变参数模板
一个可变参数模板就是一个接受可变数目参数的模板函数或模板类,可变数目的参数被称为参数包,分为模板参数包和函数参数包
template <typename T, typename... Args> //使用...来指出一个模版参数/函数参数表示一个包
void foo(const T &t, const Args &... rest) //Args是一个模板参数包,rest是一个函数参数包
{
cout << "sizeof...(Args):" << sizeof...(Args) << endl; //sizeof...()可以返回参数包中的元素数目
cout << "sizeof...(rest):" << sizeof...(rest) << endl;
}
1、编写可变参数函数模板
可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后剩余实参调用自身
//用来终止递归并打印最后一个元素的print
//此函数必须在可变参数版本的print定义之前声明
template <typename T>
ostream &print(ostream &os, const T &t) { return os << t; }
//包中除了最后一个元素之外的其他元素都会调用这个版本的print
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args &... rest)
{
os << t << " "; return print(os, rest...) //递归调用打印其他实参
}
2、包扩展
对于一个参数包,只能获取大小和进行扩展,扩展包就是将...
分解为构成的元素,并对每个元素独立应用模式,获得扩展后的列表
//1中的print调用print(cout, 12, "ok", 5.0)时
//会吧const Args &... rest扩展为const int &, const char (&)[3], const double &,即就是包扩展
template <typename… Args>
ostream &errorMsg(ostream &os, const Args&… rest)
{
return print(os, debug_rep(rest)…); //debug_rep(rest)…包扩展后就是print(os, debug_rep(a1), debug_rep(a2)…..debug_rep(an)),扩展函数调用
}
3、转发参数包
新标准下,可以组合使用可变参数模板与forward
机制编写函数,实现将其实参不变地传递给其他函数
template <typename T>
class Vec
{
...
template <typename... Args> void emplace_back(Args&&...args);
}
template <typename T>
template <typename... Args>
void Vec<T>::emplace_back(Args&&...args) //Args&&...获取取得的参数包每一个参数的完整属性
{
chk_n_alloc();
//std::forward<Args>(args)...按顺序每一个参数都完美转发
//此扩展生成此模式的元素std::forward<Ti>(ti)
alloc.construct(first_free++, std::forward<Args>(args)...);
}
Vec<string> s;
s.emplace_back(10,'c');
//理解make_shared()函数的工作过程,练习16.60-16.61:
template <typename T, typename... Args> //可变参数模板
SP<T> make_SP(Args&&... args) //完整获取参数包每个参数的属性,SP理解为shared_ptr
{
//new一个T类型的对象并自动调用其构造函数,将参数包完整转发给构造函数
//new之后自动调用SP<T>的构造函数接收new指针,并返回
return SP<T>(new T(std::forward<Args>(args)...));
}
五、模板特例化
编写单一模板来对任何可能的实参都是最合适的是很难做到的,此时可以对模板特例化出想要的版本,一个特例化版本就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型
1、定义函数模板特例化
特例化一个模板时必须为原模版中的每个模板参数都提供实参,并且在template
后跟一对空尖括号<>
template <typename T> int compare(const T &, const T &);
template <size_t N, size_t M> int compare(const char (&)[N], const char (&)[M]); //非特例化,处理常量
template <> int compare(const char* const &p1, const char* const &p2); //特例化,处理字符数组指针
//由于要与const T&, const T&对应,即传入一个T的const的引用,而我们希望T被特例化为const char *
//因此定义为const char* const &,传入的是一个const的指针的引用,指针指向const char
当定义一个特例化版本时,函数参数类型必须与先前声明的模板中对应的类型匹配
2、函数重载与模板特例化
特例化的本质是实例化一个模板,而非重载它,因此特例化本身不影响函数匹配,但是决定重载一个函数还是从模板特例化一个函数则会影响匹配
注意:为了特例化一个模板,原模板的声明必须在作用域中,而且在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中;模板及其特例化版本应该声明在同一个头文件中,并且所有同名模板的声明应该放在前面,然后是这些模板的特例化版本
3、类模板特例化
类模板template <> class ABC<string> {...}
将ABC
类特例化为string
版本
4、类模板部分特例化
与函数模板不同,类模板的特例化可以只为部分模板参数提供特例化实参,部分特例化后的类模板依然是模板,使用时用户必须为特例化过程中未指定实参的模板参数提供实参
//原始、最通用版本
template <typename T> struct remove_reference { typedef T type; }
//部分特例化版本,分别用于左值引用和右值引用
template <typename T> struct remove_reference<T&> { typedef T type; }
template <typename T> struct remove_reference<T&&> { typedef T type; }
//特例化版本的模板参数数目相同,但是类型更加限制
int i;
remove_reference<decltype(42)>::type a; //decltype(42)为int,故此时使用原始版本
remove_reference<decltype(i)>::type b; //decltype(i)为int &,故此时使用特例化的T&版本
remove_reference<decltype(std::move(i))>::type c; //decltype(std::move(i))为int &&,故此时使用T&&版本
5、特例化成员而不是类
可以只特例化部分成员函数而不是特例化整个模板
template <typename T> struct Foo
{
Foo(const T &t = T()) : mem(t) { }
void Bar() {...}
T mem;
};
template <> void Foo<int>::Bar() {...} //特例化Foo<int>的成员Bar,对int做特殊处理
Foo<string> fs; //实例化Foo<string>::Foo()
fs.Bar(); //实例化Foo<string>::Bar()
Foo<int> fi; //实例化Foo<int>::Foo()
fi.Bar(); //使用特例化版本的Foo<int>::Bar()
//为什么需要特例化:
template <typename T> bool compare(const T &, const T &); //通用比较,但是传入字符串时不可行
//需要特例化字符串的比较函数
template <size_t N, size_t M> bool compare(const char(&)[N], const char(&)[M]); //对字符数组的处理
template <> bool compare(const char * const &, const char * const &); //对字符指针的处理
const char *p1 = "hi", *p2 = "mom";
compare(1, 5); //int值的直接比较,自动调用第一个compare实例化出int的比较
compare("hi", "mom"); //字符数组的比较,调用第二个compare
//若第三个不是模板而是普通函数,则此时会选择第三个compare,选择最特例化的版本
compare(p1, p2); //若没有第三个compare,将自动调用第一个并传入指针
//而我们希望比较字符串而不是指针的值,因此需要特例化一个compare