模板特例化由来
单一模板对任何可能的模板实参并总是合适,并且能实例化的。比如,compare的例子,用于比较2个实参:
// 第一个版本:比较任意两个类型
template <typename T>
int compare(const T&, const T&);
// 第二个版本:可以比较字符串字面量
template <size_t N, size_t M>
int compare(const char (&)[N], const char (&)[N]);
第一个版本compare可用于比较任意两个类型,第二个版本可以用于比较字符串字面量。然而,当实参是2个字符串对应指针时,我们希望的是比较2个字符串大小,而第一个版本会直接比较2个指针大小,第二个版本无法将指针转化为一个数组的引用(因为编译器无法直接通过实参指针判断这是个什么类型)。因此,这2个版本compare都是不行的。
为了解决这个字符指针的问题,我们引入模板特例化(template specialization)。一个特例化版本就是模板的一个独立定义,其中一个或多个目标参数被指定为特定的类型。另外,模板特例化在有的书籍简称“模板特化”。
定义函数模板特例化
可以为第一个版本的compare定义一个模板特例化版本。以template<>开头,“<>” 表示我们将为原模板的所有模板参数提供实参:
// compare的特例化版本,用于处理字符串数组的指针的比较
template<>
int compare(const char* const &p1, const char* const &p2)
{
return strcmp(p1, p2);
}
当我们定义一个特例化版本时,函数参数类型必须与先前声明的模板中对于的类型匹配。我们特例化的原先模板:
template <typename T>
int compare(const T&, const T&);
定义的特例化中,T为const char *
(底层const),第一个版本中const是用来表示形参a为const &
(顶层const),如果直接写成const (const char*) &a
,则等价于const char * &a
,并不能表示引用本身无法修改,因此将const移动到形参a处(顶层const),最终结果就是const char* const& a
。
函数重载与模板特例化
定义一个函数模板的特例化版本本质上是为原模板的一个特殊实例提供定义,而非函数名的一个重载版本。
当对字符串字面常量调用compare时,
compare("hi", "mom");
compare的版本二和版本三特例化版本都可行,提供同样精确的匹配,但是接受字符数组参数的版本更特例化,因此编译器或选择版本二。
如果将接受字符指针的compare版本修改定义为一个普通的非模板函数,在通用精确的匹配时,编译器会优先选择非模板函数。
Tips:模板及其特例化版本应该声明在同一个头文件中,位于同一个作用域。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。避免位于不同文件,特例化版本丢失时(或者不在同一个作用域),编译器依然可以用原有的模板进行匹配,不过可能并不是我们想要的结果。这种问题通常难以查找。
类模板特例化
前面讲了函数模板可以特例化,类模板同样也可以特例化。STL hash模板可以用于计算对象的hash值,我们以hash模板定义一个特例化版本为例,用它来保存Sales_data对象。
一个特例化hash类必须定义:
- 一个重载的调用运算符operator(),它接受一个容器关键字类型的对象,为给定类型计算hash值,返回一个size_t;
- 两个类型成员:result_type和argument_type,分别调用运算符的返回类型和参数类型;
- 默认构造函数和拷贝赋值运算符;
参考C++ hash(STL hash)及其函数模板用法详解
template <class T> class std::hash; // 声明友元函数所需
class Sales_data {
friend class std::hash<Sales_data>;
private:
string bookNo;
unsigned units_sold;
double revenue;
};
// 打开std命令空间,特例化std::hash
namespace std {
template<>
struct hash<Sales_data>
{
typedef size_t result_type;
typedef Sales_data argument_type;
size_t operator()(const Sales_data& s) const;
};
size_t hash<Sales_data>::operator()(const Sales_data& s) const
{
return hash<string>()(s.bookNo) ^
hash<unsigned>()(s.units_sold) ^
hash<double>()(s.revenue);
}
} // 关闭std命名空间
// 客户端,利用特化版本hash<Sales_data>求Sales_data对象hash值template<>
struct my_is_void<void> {
static const bool value = true;
};
int main()
{
auto p = new hash<Sales_data>();
Sales_data s;
size_t d = p->operator()(s);
cout << d << endl;
d = hash<Sales_data>()(s);
cout << d << endl;
return 0;
}
类模板部分特例化
与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非所有模板参数,或者参数的一部分而非全部特性。一个类模板的部分特例化(partial specialization,也称\(\color{red}{偏特化}\))本身是一个模板,使用它时用户必须为那些在特例化版本中未指定的目标参数提供实参。
// 以下内容来自STL库
// 原始的、最通用的版本
// STRUCT TEMPLATE remove_reference
template<class _Ty>
struct remove_reference
{ // remove reference
using type = _Ty;
};
// 部分特例化版本,用于左值引用和右值引用
template<class _Ty>
struct remove_reference<_Ty&> // 只适用于左值引用的特例化版本
{ // remove reference
using type = _Ty;
};
template<class _Ty>
struct remove_reference<_Ty&&> // 只适用于右值引用的特例化版本
{ // remove rvalue reference
using type = _Ty;
};
部分特例化的“部分”在于模板参数列表是原始模板的参数列表的一个子集,或者一个特例化版本。\(\color{red}{所谓子集,包含2个方面:模板参数的数目,模板参数的类型。}\)本例中,特例化版本的模板参数数目与原始版本相同,但类型不同,属于原始版本的子集。
两个特例化版本分别只能用于左值引用和右值引用:
int i;
// decltype(42)为int,使用原始模板
remove_reference<decltype(42)>::type a;
// decltype(i)为int&,使用第一个(T&)部分特例化版本
remove_reference<decltype(i)>::type b;
// decltype(std::move(i))为int&&,使用第二个(T&&)部分特例化版本
remove_reference<decltype(std::move(i))>::type c;
a,b,c均为int类型。
特例化成员而不是类
可以只特例化成员函数而不特例化整个模板。
例如,只特例化类模板Foo的成员函数Bar:
template <typename T>
struct Foo {
Foo(const T& t = T ()) : mem(t) { }
void Bar() { /* ... */ } // 通用Bar函数版本
T mem;
// ...
};
// 类模板Foo的Bar成员特例化版本, 只有Foo<int>对象,才会调用该特例化函数版本. 其他的调用通用Bar函数
template<>
void Foo<int>::Bar()
{
// 进行T为int的特例化处理
}
// 客户端
Foo<string> fs; // 实例化Foo<string>::Foo()
fs.Bar(); // 实例化Foo<string>::Bar()
Foo<int> fi; // 实例化Foo<int>::Foo()
fi.Bar(); // 使用特例化函数版本Foo<int>::Bar()
模板特例化与模板实例化
注意区分模板特例化与模板实例化。
-
模板实例化
是指编译器根据根据调用函数给定的函数实参,或者定义类时给定的显示模板参数列表,推断出对应模板参数类型,并用实参类型替换模板参数,创建出一个新的“实例”,这个过程叫模板实例化。
特征:任何模板(函数模板或类模板),都必须实例化后才能被用户使用。 -
模板特例化
是指为解决通用模板不能很好解决特定类型匹配问题,而专门设置的独立模板。
即使是模板特化,也许需要实例化,才能为用户所使用;但即使没有特化版本,也可以匹配通用版本的模板,然后实例化为用户所用。
特征:
1)必须有通用的函数模板或类模板,特例化只针对少数特定类型额外定义的模板;
2)形式上以template<>开头;
模板特例化与模板偏特化
模板特化是为通用模板定义一个特殊情况,模板参数是某个固定的具体类型,比如void,char,string等(每个特化模板只能固定一个)。函数模板,类模板都可以特例化。
偏特化的模板参数是通用模板参数的子集(个数和类型),并不是固定的具体类型,而是一类类型,比如T可以匹配char, int,string等(每个偏特化模板可以匹配一类) 。只能偏特化类模板,不能偏特化函数模板。为什么?因为针对参数类型不同、个数不同的函数,完全可以用函数重载来实现,也就没有必要对函数模板偏特化。
// 特化例子
template<typename T>
struct my_is_void {
static const bool value = false;
};
template<>
struct my_is_void<void> { //特化版本,只有my_is_void<void>(模板参数是void)的对象,才能匹配到该特化版本
static const bool value = true;
};
// 偏特化例子
template<typename T>
struct my_is_pointer {
static const bool value = false;
};
template<typename T>
struct my_is_pointer<T*> { // 偏特化版本,任意my_is_pointer<T*>(模板参数是指针类型)的对象,才能匹配偏特化版本
static const bool value = true;
};
int main()
{
my_is_void<bool> t1; // 使用通用版本my_is_void实例化,T为bool
cout << t1.value << endl; // 打印0
my_is_void<void> t2; // 使用特例化版本my_is_void实例化,T为void
cout << t2.value << endl; // 打印1
my_is_pointer<char> p1; // 使用通用版本my_is_pointer实例化,T为char
cout << p1.value << endl; // 打印0
my_is_pointer<char *> p2; // 使用偏特化版本my_is_pointer实例化,T为char*
cout << p2.value << endl; // 打印1
return 0;
}