上文我们学习了类模板的特化,我们可以针对复合类型(指针、引用、数组)或特定类型进行部分特化或完全特化定义。源代码文件中的类模板的定义就如同一个switch语句,基本定义是default分支,每个特化都是一个case。你可以想像编译器在编译我们的代码时自动把同一命名空间中的某个类模板的所有定义(基本定义,部分特化,完全特化)形成一个switch语句,然后根据我们对类模板的实际使用方式一一找到定义最匹配的那个case。这样我们可以轻松应对通用情况和各种特定使用场景。一旦需求有改动或者添加,我们只需要面对那一个case,其他部分无需操心。
类模板的部分特化在实际应用中更多的是多类型参数情形,特别是元编程,部分特化的作用是类型定义的递归,类似于函数式编程中的循环,而完全特化的作用则是结束递归的条件判断。既然部分特化的应用范围比较广,所以这里想再多说几句。
还是用那个旧例子吧,max,熟人好办事嘛。
复制内容到剪贴板
代码:
// #1
template<typename T, typename U>
class math {
public:
static auto max(const T a, const U b) {
return b < a ? a : b;
}
};
这次的math类型名跟上次的不一样,上次是math<T>,这次是math<T, U>,模板类型参数列表中有两个类型参数,这样max的定义中两个参数的类型可以是不一样了。下面我们随便定义几个部分特化,定制某些特定需求。
复制内容到剪贴板
代码:
// #2
template <typename T>
class math<T, T>{
};
// #3
template <typename T>
class math<T, int>{
};
// #4
template<typename T, typename U>
class math<T*, U*>{
};
#2针对两个模板参数类型相同时的特化,#3是对第二个模板类型参数为int时的特化,#4则是对两个模板参数类型都是指针时的特化。部分特化的定义用到几个模板类型参数就在template中引入几个参数,math的类型名必须都是一样的,math<类型1,类型2>,里面的类型既可以是模板参数列表中的,也可以是某个确定类型,还可以是由类型参数形成的复合类型。
好了,下面举几个例子,看看编译器会给咱们选定哪个定义版本。
复制内容到剪贴板
代码:
math<int, float>::max // #1
math<float, float>::max // #2
math<float, int>::max // #3
math<int*, float*>::max // #4
上面四个的使用定义版本应该很明显,一目了然。好了,再看一个
复制内容到剪贴板
代码:
math<int, int>::max
这个应该应用哪个版本呢?#2吧?很有道理,因为两个模板类型参数是相同的,都是int。但是再仔细看看,#3也同样匹配,它的特化是第二个参数是int。这次#2和#3的匹配度是一样的,所以编译器无法选择,只能告诉你max定义有多义性。如何解决呢?没有办法,只能定义一个完全特化版本math<int, int>,它比#2和#3的匹配度更高,这个时候编译器会选定它。
部分特化就先说这么多吧,后面SFINAE和元编译用到时会更加有趣,一起加油。