【C++基础】第九十七课:[模板与泛型编程]模板特例化

模板特例化

Posted by x-jeff on May 8, 2024

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.模板特例化

编写单一模板,使之对任何可能的模板实参都是最适合的,都能实例化,这并不总是能办到。在某些情况下,通用模板的定义对特定类型是不适合的:通用定义可能编译失败或做得不正确。其他时候,我们也可以利用某些特定知识来编写更高效的代码,而不是从通用模板实例化。当我们不能(或不希望)使用模板版本时,可以定义类或函数模板的一个特例化版本。

我们的compare函数是一个很好的例子,它展示了函数模板的通用定义不适合一个特定类型(即字符指针)的情况。我们希望compare通过调用strcmp比较两个字符指针而非比较指针值。实际上,我们已经重载了compare函数来处理字符串字面常量(参见:函数模板):

1
2
3
4
5
//第一个版本:可以比较任意两个类型
template <typename T> int compare(const T&, const T&);
//第二个版本处理字符串字面常量
template<size_t N, size_t M>
int compare(const char (&)[N], const char (&)[M]);

但是,只有当我们传递给compare一个字符串字面常量或者一个数组时,编译器才会调用接受两个非类型模板参数的版本。如果我们传递给它字符指针,就会调用第一个版本:

1
2
3
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2); //调用第一个模板
compare("hi", "mom"); //调用有两个非类型参数的版本

我们无法将一个指针转换为一个数组的引用,因此当参数是p1和p2时,第二个版本的compare是不可行的。

为了处理字符指针(而不是数组),可以为第一个版本的compare定义一个模板特例化(template specialization)版本。一个特例化版本就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型。

1.1.定义函数模板特例化

当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应使用关键字template后跟一个空尖括号对(<>)。空尖括号指出我们将为原模板的所有模板参数提供实参:

1
2
3
4
5
6
//compare的特殊版本,处理字符数组的指针
template <>
int compare(const char* const &p1, const char* const &p2)
{
	return strcmp(p1, p2);
}

理解此特例化版本的困难之处是函数参数类型。当我们定义一个特例化版本时,函数参数类型必须与一个先前声明的模板中对应的类型匹配。本例中我们特例化:

1
template <typename T> int compare(const T&, const T&);

1.2.函数重载与模板特例化

特例化的本质是实例化一个模板,而非重载它。因此,特例化不影响函数匹配。

我们将一个特殊的函数定义为一个特例化版本还是一个独立的非模板函数,会影响到函数匹配。例如,我们已经定义了两个版本的compare函数模板,一个接受数组引用参数,另一个接受const T&。我们还定义了一个特例化版本来处理字符指针,这对函数匹配没有影响。当我们对字符串字面常量调用compare时:

1
compare("hi", "mom")

对此调用,两个函数模板都是可行的,且提供同样好的(即精确的)匹配。但是,接受字符数组参数的版本更特例化(参见:重载与模板),因此编译器会选择它。

如果我们将接受字符指针的compare版本定义为一个普通的非模板函数(而不是模板的一个特例化版本),此调用的解析就会不同。在此情况下,将会有三个可行的函数:两个模板和非模板的字符指针版本。所有三个函数都提供同样好的匹配。如前所述,当一个非模板函数提供与函数模板同样好的匹配时,编译器会选择非模板版本(参见:重载与模板)。

关键概念:普通作用域规则应用于特例化

为了特例化一个模板,原模板的声明必须在作用域中。而且,在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中。

对于普通类和函数,丢失声明的情况(通常)很容易发现——编译器将不能继续处理我们的代码。但是,如果丢失了一个特例化版本的声明,编译器通常可以用原模板生成代码。由于在丢失特例化版本时编译器通常会实例化原模板,很容易产生模板及其特例化版本声明顺序导致的错误,而这种错误又很难查找。

如果一个程序使用一个特例化版本,而同时原模板的一个实例具有相同的模板实参集合,就会产生错误。但是,这种错误编译器又无法发现。

模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。

1.3.类模板特例化

除了特例化函数模板,我们还可以特例化类模板。作为一个例子,我们将为标准库hash模板定义一个特例化版本,可以用它来将Sales_data对象保存在无序容器中。默认情况下,无序容器使用hash<key_type>来组织其元素。为了让我们自己的数据类型也能使用这种默认组织方式,必须定义hash模板的一个特例化版本。一个特例化hash类必须定义:

  • 一个重载的调用运算符,它接受一个容器关键字类型的对象,返回一个size_t。
  • 两个类型成员,result_type和argument_type,分别调用运算符的返回类型和参数类型。
  • 默认构造函数和拷贝赋值运算符(可以隐式定义)。

在定义此特例化版本的hash时,唯一复杂的地方是:必须在原模板定义所在的命名空间中特例化它。为了达到这一目的,首先必须打开命名空间:

1
2
3
//打开std命名空间,以便特例化std::hash
namespace std {
} //关闭std命名空间;注意:右花括号之后没有分号

花括号对之间的任何定义都将成为命名空间std的一部分。下面的代码定义了一个能处理Sales_data的特例化hash版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//打开std命名空间,以便特例化std::hash
namespace std {
template <> //我们正在定义一个特例化版本,模板参数为Sales_data
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命名空间;注意:右花括号之后没有分号

注:通过^运算符将多个哈希值组合成一个单一的哈希值。

假定我们的特例化版本在作用域中,当将Sales_data作为容器的关键字类型时,编译器就会自动使用此特例化版本:

1
2
//使用hash<Sales_data>和Sales_data的operator==
unordered_multiset<Sales_data> SDset;

由于hash<Sales_data>使用Sales_data的私有成员,我们必须将它声明为Sales_data的友元:

1
2
3
4
5
template <class T> class std::hash; //友元声明所需要的
class Sales_data {
friend class std::hash<Sales_data>;
	//其他成员定义,如前
};

这段代码指出特殊实例hash<Sales_data>是Sales_data的友元。由于此实例定义在std命名空间中,我们必须记得在friend声明中应使用std::hash

1.4.类模板部分特例化

与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的部分特例化(partial specialization)本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。

我们只能部分特例化类模板,而不能部分特例化函数模板。

我们介绍了标准库remove_reference类型,该模板是通过一系列的特例化版本来完成其功能的:

1
2
3
4
5
6
7
8
9
//原始的、最通用的版本
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&&> //右值引用
	{ typedef T type; };

1.5.特例化成员而不是类

我们可以只特例化特定成员函数而不是特例化整个模板。例如,如果Foo是一个模板类,包含一个成员Bar,我们可以只特例化该成员:

1
2
3
4
5
6
7
8
9
10
11
template <typename T> struct Foo {
	Foo(const T &t = T()) : mem(t) { } //t的默认值为T(),其调用T的默认构造函数
	void Bar() { /*...*/ }
	T mem;
	//Foo的其他成员
};
template<> //我们正在特例化一个模板
void Foo<int>::Bar() //我们正在特例化Foo<int>的成员Bar
{
	//进行应用于int的特例化处理
}

本例中我们只特例化Foo<int>类的一个成员,其他成员将由Foo模板提供:

1
2
3
4
Foo<string> fs; //实例化Foo<string>::Foo()
fs.Bar(); //实例化Foo<string>::Bar()
Foo<int> fi; //实例化Foo<int>::Foo()
fi.Bar(); //使用我们特例化版本的Foo<int>::Bar()