1、模板特例化
1.1 什么是模板实例化?为什么有这种东西?
某些情况下,通用模板的定义对特定类型是不合适的,他有可能发生编译失败或做的不正确。就比如说之前编写的compare
函数模板。
起初编写了:
template<typename T> int compare(const T& t1, const T& t2)
{
if (t1 < t2) return 1;
if (t2 < t1) return -1;
reutrn 0;
}
这个模板就不能用于比较字面值常量:
compare("hello", "world"); 错误,小于号怎么能比较两个字面值常量?应该用 strcmp() 比较
所以后来又定义了第二个版本处理字面值常量:
template<size_t N, size_t M> int compare(const char(&t1)[N], const char(&t2)[M])
{
return strcmp(t1, t2);
}
现在虽然能比较两个字面值常量了,但还有一种情况处理不了:
const char* p1 = "hi", *p2 = "mom";
compare(p1, p2);
上面的调用虽然精准的匹配compare
第一个版本,但是指针传进去没法比啊,比较两个指针是无意义的啊,还是要用strcmp
来比才行。
- 这时就轮到模板特例化出场了。
为了处理字符指针(不是数组),为第一个版本的compare
定义一个模板特例化版本。特例化的本质是实例化一个模板,让他处理某种特殊情况。
1.2 定义函数模板特例化
当特例化一个函数模板时,必须为原模板中每个模板类型参数都提供实参。为了指出我们正在实例化一个模板,应使用关键字template
后跟一对空尖括号:
template<> int compare(const char* const & p1, const char* const & p2) 这参数类型咋这老复杂?不怕,且听下面的分析!
{
return strcmp(p1, p2);
}
这个模板特例化的逻辑很简单,就是比较传入的两个指针指向的字符串。但困难的是:如何理解这复杂的参数类型?
先从这个特例化的来源分析。当定义一个模板的特例化版本时,特例化的函数参数类型必须与模板中对应的类型匹配。本例中,我们特例化的是这个模板:
template<typename T> int compare(const T&, const T&);
其中,compare
函数的参数是一个 const
类型的引用,也就是说:我们最终特例化出来的compare
函数的参数类型至少是一个const
的引用,除此之外,还应该算上传入参数的类型,传入的参数T
也有自己的类型,所以最终的函数参数类型是很复杂的。本例的结果也确实是这样。
我们想要特例化的版本中,传入的参数类型是指向char
的常量指针,这就是T
的类型,也就是const char*
类型。而模板中,函数参数的类型又要求T
是一个const
类型的引用。所以,最终特例化出来的函数的参数类型要将两者结合起来,也就是:
const char* const & 类型
即,一个指向const char
的const
指针的引用。
1.3 重载 与 函数模板特例化
上面提到过,特例化的本质是实例化一个模板。
当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。即,我们自己为原模板实例化出一个实例。
说上面这些,重要的是弄清楚:特例化版本终究只是原模板的某个实例,因此,他并不是同名函数的重载版本,所以他不会影响函数匹配。
既然特例化不参与函数重载,那么这里为什么要提呢?问得好哈哈哈!
试想,上面的特例化版本是不是完全可以定义为一个非模板的普通函数呢?
确实可以欸。特例化不就是为了处理某种特殊情况的吗?当然可以不搞特例化,而直接定义一个普通函数。但是,普通函数和特例化可不完全一样,普通函数可是会参与到函数重载的啊!它会影响函数匹配!
接上面compare
的例子,已经为compare
定义了两个模板和一个特例化了(忘了就到上面复习去)。特例化不会影响函数匹配,当进行如下调用时:
compare("hi", "mom");
对此调用,两个模板都提供精确匹配。但是,接受字符数组的版本更特例化,所以编译器会选它。从这里也可以看到,根本没有特例化版本的戏份,也侧面证明了特例化不会影响函数匹配。
但如果把接受字符串指针的compare
定义为一个非模板普通函数,而非特例化版本,那么对此调用的解析就会不同。此时,会有三个可行的函数,两个模板和一个非模板函数,且他们都提供同样好的匹配,但是非模板函数更特例化,最终选择非模板版本。
既然特例化不影响函数匹配,那么,到底什么时候会考虑调用特例化版本呢???
书上没说,不过我有个猜想。特例化是从其模板特例化而来的,那么就应当是:当某个调用匹配到了该模板(别的可选重载都被筛下去了,别无他法只能选这个比较通用的模板了,即使里面的代码并不能正确的处理此调用),然后发现,欸?这个模板的特例化不是正好可以精确匹配吗(喜出望外),好!那就调用这个特例化吧。先进行函数匹配,匹配到一个模板之后,再考虑是否使用特例化版本。也就是说:当调用到特例化的时候,函数匹配早就做完了。
1.4 类模板特例化
除了特例化函数模板,还可以特例化类模板。作为例子,下面为标准库的hash
模板定义一个特例化版本,进而,才可以把Sales_data
对象保存在无序容器中了。
啊?这么麻烦吗?不特例化就不能用无需容器存Sales_data
了吗?
确实不能。在使用无序容器时,需要根据对象的值计算出一个哈希值,然后根据这个哈希值组织容器内的元素。对于内置类型,标准库已经特例化了对应的hash<key_type>
版本,可以计算出他们的哈希值。而对于一个Sales_data
对象,标准库无从计算他的哈希值啊,所以就没办法存Sales_data
类型。所以我们要特例化一个hash<Sales_data
来组织其元素。
一个特例化hash
类必须定义:
- 一个重载的调用运算符,接受一个容器键值类型(
key_type
)的对象,返回一个size_t
- 两个类型成员,
result_type
和argument_type
,分别是调用运算符的返回值类型和参数类型的别名 - 默认构造函数和拷贝复制运算符
在定义特例化版本的hash
时,要注意下面这条原则:
模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。
由于哈希的模板在std
命名空间中,所以我们要进行如下操作:
打开 std 命名空间,以便特例化 std::hash
namespace std
{
// ........ 自定义的内容
} 关闭命名空间,注意不用写分号
下面的代码定义了一个能处理Sales_data
的特例化hash
版本:
namespace std
{
template<> struct hash<Sales_data>
{
typedef size_t result;
typedef Sales_data argument_type;
size_t operator()(const Sales_data&) 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);
}
}
hash<Sales_data>
的定义以template<>
开始,指出我们正在定义一个全特例化的实例。特例化的版本为hash<Sales_data>
。
和普通类一样,可以在类内或类外定义特例化版本的成员,本例是在类外定义的。重载的调用运算符必须为给定类型的值定义一个哈希函数。且对于一个给定值,该函数总能返回相同的结果。本例中,我们把求哈希值的复杂任务交给了标准库:我们使用未命名的——
hash<string>
对象生成bookNo
的哈希值hash<unsigned>
对象生成units_sold
的哈希值hash<double>
对象生成revenue
的哈希值
我们把这些结果进行异或运算,形成给定Sales_data
的完整哈希值。
还没完,由于hash<Sales_data>
使用了Sales_data
的私有成员,还要把他声明为Sales_data
的友元:
template<class T> class std::hash; 友元声明所需
class Sales_data
{
friend class std::hash<Sales_data>;
// ...... 其他成员省略
};
1.5 类模板部分特例化
与函数模板不同,类模板的特例化不必为所有模板参数提供实参。可以只指定 一部分而非所有模板参数,或是参数的一部分而非全部特性。这时,一个类模板的部分特例化本身仍然是一个模板,使用它时用户必须为那些在特例化版本中未指定的模板参数提供实参。
部分特例化最典型的应用就是标准库remove_reference
类型,该模板通过一系列的特例化版本完成其功能:
原始版本:
template<class T> struct remove_reference
{
typedef T type;
};
两个 部分特例化版本,分别用于左值引用和右值引用:
template<class T> struct remove_reference<T&>
{
typedef T type;
};
template<class T> struct remove_reference<T&&>
{
return T type;
};
第一个模板定义了最通用的模板。它可以用任意类型实例化,他把模板实参作为type1
成员的类型。后面两个都是她的特例化版本。
由于一个部分特例化版本 其本质还是个模板,所以先定义模板参数(就是第一个尖括号里的)。对于每一个未完全确定类型的模板参数,在特例化版本的模板参数列表中都有一项与之对应。如果全确定了,那就是空尖括号(参考上一小节的例子),不过那是完全特例化版本,我们这里讲的是部分特例化,所以不会出现空尖括号的情况。在本例中两个特例化的版本都没有确定模板参数,他们只是 指定了参数的一部分特性。所以,类名之前的部分都与原模板一致。
在类名之后,需要为特例化的模板参数指定实参,这些实参列于模板名之后的尖括号中,与原始模板中的参数按位置一一对应。
部分特例化版本的模板参数列表是原始模板的参数列表的一个子集或是指定了参数的一部分特性。
部分特例化模板的构造大致如下:
template<这里列出特例化版本中没有确定类型的模板参数> class 模板名<这里列出确定类型的模板参数 以及 指定了部分特性的模板参数>
{
定义你的类吧
};
本例中,特例化版本的参数指定了模板参数的部分特性。两个特例化版本分别用于左值引用和右值引用类型:
int i;
int& r = i;
remove_reference<decltype(42)>::type a; 虽说42是个右值,但decltype(42)是int,不可能是int&&,所以会调用原始模板
remove_reference<decltype(i)>::type b; 虽说 i 是个左值,但decltype(i)是int,不可能是int&,所以也会带调用原始版本
remove_reference<decltype(r)>::type c; r 是左值引用,理所当然会调用接收左值引用的版本
remove_reference<decltype(std::move(i))>::type d; 调用接受右值引用的版本
1.6 特例化模板的某个成员
例题一:16.66 重载函数与特例化他相比,有何优缺点?
当调用到特例化的时候,函数匹配早就做完了。
上面这段话分析的非常好!!!