7.6 模板特性
模板头文件包含函数模板和类模板成员函数的定义
通常我们调用一个函数时,编译器只需要掌握函数的声明。类似地,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。
模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此模板的头文件通常既包括声明,也包括定义。
模板实例化
1. 编译器在模板实例化时才生成代码
当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到。
Tips:需要注意的是,如果类模板的一个成员函数没有被使用,则它不会被实例化。成员函数只有在被用到时才进行实例化,这一特性使得即使某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类。
通常情况下编译器会在三个阶段报告错误:
- 第一个阶段是编译模板本身时:编译器检查语法错误,例如忘记分号或者变量名拼错等
- 第二个阶段是编译器遇到模板使用时:检查实参数目是否正确、参数类型是否匹配
- 第三个阶段是模板实例化时:只有这个阶段才能发现类型相关的错误,依赖于编译器如何管理实例化,这类错误可能在链接时才报告
2. 控制实例化
当模板被使用时才会进行实例化这一特性意味着:相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在C++11新标准中,我们可以通过显式实例化来避免这种开销。显式实例化有下面两种形式:
extern template declaration; // 实例化声明
template declaration; // 实例化定义
当编译器遇到extern
模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern
就表示承诺在程序其他位置有该实例化的一个非extern
声明(定义)。对于一个给定的实例化版本,可能有多个extern
声明,但必须只有一个定义:
// 这些模板类型必须在程序其他位置进行实例化
extern template class Foo<string>;
extern template int compare(const int&, const int&);
实例化文件必须为每个在其他文件中声明为extern
的类型和函数提供一个(非extern
)的定义:
// 实例化compare的int版本
template int compare(const int&, const int&);
// 实例化类模板的所有成员
template class Foo<string>;
3. 类模板的实例化定义会实例化所有成员
一个类模板的实例化定义会实例化该模板的所有成员,包括内敛的成员函数。当编译器遇到一个实例化定义时,他不了解程序使用哪些成员函数,因此与处理类模板的普通实例化不同,编译器会实例化该类的所有成员。即使我们不使用某个成员,它也会被实例化,因此显式实例化一个类模板时必须能用于模板的所有成员。
模板特例化
1. 简介
编写单一模板,使之对任何可能的模板实参都是最合适的,都能实例化,这并不总是能办到。当我们不能(或不希望)使用模板版本时,可以定义类或函数模板的一个特例化版本。
2. 函数模板特例化
举个例子,我们构造一个compare
函数用于比较不同类型的大小,并重载了compare
函数来处理字符串字面常量:
// 函数模板一: 可以比较任意两个类型
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
一个字符串字面常量或者一个数组时,编译器才会调用接收两个非类型模板参数的版本。如果我们传递给它字符指针,它就会调用第一个版本(因为我们无法将一个指针转换成一个数组的引用):
const char *p1 = "tomo", *p2 = "cat";
compare(p1, p2); // 调用第一个模板
compare("tomo", "cat"); // 调用第二个模板
Tips:当定义函数模板的特例化版本时,我们本质上接管了编译器的工作,即我们为原模板的一个特殊实例提供了定义。
为了处理字符指针(而不是数组),可以为第一个版本的compare
定义一个模板特例化版本。当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应该使用关键字template
后跟一个空尖括号对<>
:
// 函数模板
template <typename T>
int compare(const T&, const T&);
// compare的特殊版本, 处理字符数组的指针
template <>
int compare(const char* const &p1, const char* const &p2) {
return strcmp(p1, p2);
}
需要注意的是,一个特例化版本本质上是一个实例,而非函数名的一个重载版本。如果我们将接收字符指针的compare
版本定义为一个普通的非模板函数(而不是函数模板的一个特例化版本),此调用的解析就会不同。在此情况下,将由三个可行的函数:两个模板和非模板的字符指针版本。
3. 类模板特例化
作为一个例子,我们为标准库hash
模板定义一个特例化,可以用它来将Foo
对象保存在无序容器中。默认情况下,无序容器使用hash<key_type>
来组织其元素,为了让我们的数据类型也能使用这种默认组织方式,必须定义hash
模板的一个特例化版本。一个hash
类必须定义:
- 一个重载的调用运算符,它接受一个容器关键字类型的对象,返回一个
size_t
- 两个类型成员,
result_type
和argument_type
,分别是调用运算符的返回类型和参数类型 - 默认构造函数和拷贝赋值运算符(可以隐式定义)
在定义此特例化版本的hash
时,唯一复杂的地方是:必须在原模板定义所在的命名空间中特例化它。为了达到这一目的,首先必须打开命名空间:
struct Foo {
string str;
double d;
};
// 打开std命名空间, 以便特例化std::hash
namespace std {
template <> // 我们正在定义一个特例化版本, 模板参数为Foo
struct hash<Foo> {
// 用来散列一个无序容器的类型必须要定义下列类型
typedef size_t result_type;
typedef Foo argument_type;
size_t operator()(const Foo& foo) const;
// 我们的类使用合成的拷贝控制成员和默认构造函数
};
size_t hash<Foo>::operator()(const Foo& foo) const {
return hash<string>()(foo.str) ^
hash<double>()(s.d);
}
} // 关闭std命名空间, 注意右花括号之后没有分号
Tips:为了让
Foo
的用户能使用hash
特例化版本,我们应该在Foo
的头文件中定义该特例化版本。
4. 类模板部分特例化
Tips:我们只能部分特例化类模板,而不能部分特例化函数模板。
与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指出的模板参数提供实参。
举个例子,标准库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&&> {
typedef T type;
};
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;
5. 特例化成员函数
我们可以只特例化特定成员而不是特例化整个模板。例如对于一个包含bar()
成员模板类Foo
,我们可以只特例化该bar()
成员:
template <typename T> struct Foo {
Foo(const T &t = T()) : mem(t) { }
void Bar() { /*...*/ }
T mem;
};
// 特例化Foo<int>的Bar()成员
template <>
void 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()