在现代软件开发中,我们经常需要编写能够处理多种数据类型的通用代码。想象一下,如果我们需要为每种数据类型都编写一个单独的函数来完成相同的操作,这将是多么繁琐和容易出错的工作。 模板正是C++提供的强大机制,让我们能够编写一次代码,然后让编译器为不同的类型自动生成对应的实现。
面向对象编程和泛型编程都处理在编写程序时类型未知的情况。两者的区别在于,面向对象编程处理的类型在运行时才确定,而泛型编程中的类型在编译期间就能确定。 我们在前面章节中学习的容器、迭代器和算法都是泛型编程的典型例子。当我们使用泛型程序时,我们提供程序实例操作的类型或值。

模板是泛型编程的基础。我们可以在不理解模板如何定义的情况下使用它们,但要成为C++的高级程序员,理解模板的工作原理是必不可少的。
假设我们想要编写一个函数来比较两个数值,判断第一个数是小于、等于还是大于第二个数。在实际应用中,我们希望能够比较整数、浮点数、字符串等不同类型的数据。传统的方法是为每种类型编写一个重载函数:
|// 比较两个整数 int compareValues(const int &first, const int &second) { if (first < second) return -1; if (second < first) return 1; return 0; } // 比较两个字符串 int compareValues(const std::string &first, const std::string &second) { if (first < second) return -1; if (second < first) return 1; return 0; }
这些函数几乎完全相同,唯一的区别就是参数类型。为每种类型重复编写相同的函数体不仅繁琐,而且容易出错。更重要的是,我们需要在编写程序时就知道所有可能用到的类型,这在很多情况下是不现实的。
函数模板为我们提供了更好的解决方案。我们可以定义一个模板,让编译器根据实际使用的类型自动生成对应的函数版本:
|template <typename T> int compareValues(const T &first, const T &second) { if (first < second) return -1; if (second < first) return 1; return 0; }
模板定义以关键字template开始,后跟模板参数列表。这个参数列表是用尖括号包围的一个或多个模板参数的逗号分隔列表。在模板定义中,模板参数列表不能为空。
模板参数列表的作用类似于函数参数列表。函数参数列表定义了若干特定类型的局部变量,但不说明如何初始化它们。运行时,调用者提供实参来初始化形参。类似地,模板参数表示在类或函数定义中使用的类型或值。 当使用模板时,我们指定模板实参来绑定到模板参数上。
当我们调用函数模板时,编译器通常会使用函数调用的实参来推断模板参数。例如:
|std::cout << compareValues(10, 20) << std::endl; // T被推断为int std::cout << compareValues(3.14, 2.71) << std::endl; // T被推断为double
在第一个调用中,实参是int类型,编译器推断出T为int。在第二个调用中,实参是double类型,编译器推断出T为double。
编译器使用推断出的模板参数来实例化模板的特定版本。当编译器实例化一个模板时,它创建一个新的"实例",用实际的模板实参替换对应的模板参数。例如,给定上面的调用,编译器会实例化两个不同版本的compareValues:
|// 为第一个调用实例化的版本 int compareValues(const int &first, const int &second) { if (first < second) return -1; if (second < first) return 1; return 0; } // 为第二个调用实例化的版本 int compareValues(const double &first
这些编译器生成的函数通常被称为模板的实例化。
在我们的compareValues函数中有一个模板类型参数。一般来说,我们可以像使用内置类型或类类型说明符一样使用类型参数。特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换:
|template <typename T> T getMaxValue(T* array, size_t size) { T maxVal = array[0]; // maxVal的类型将是T for (size_t i = 1; i < size; ++i) { if (array[i] > maxVal) { maxVal = array[i]; } }
每个类型参数前必须使用关键字class或typename。这两个关键字的含义相同,可以互换使用。使用typename可能更直观,因为它清楚地表明后面跟着的是一个类型名。
除了定义类型参数,我们还可以定义非类型模板参数。非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非class或typename关键字来指定非类型参数。
例如,我们可以编写一个处理数组的函数模板:
|template<unsigned Size> int processArray(const int (&arr)[Size]) { int sum = 0; for (unsigned i = 0; i < Size; ++i) { sum += arr[i]; } return sum; }
当我们调用这个版本的processArray时:
|int data[] = {1, 2, 3, 4, 5}; int result = processArray(data); // Size被推断为5
编译器会使用数组的大小来实例化模板,Size被替换为5。
非类型参数的实参必须是常量表达式,这允许编译器在编译时实例化模板。非类型参数在模板定义内是一个常量值,可以在需要常量表达式的地方使用,例如指定数组大小。
虽然简单,我们的compareValues函数说明了编写泛型代码的两个重要原则。首先,模板中的函数参数是对const的引用。其次,函数体中的条件判断仅使用<比较运算。这些设计选择使得我们的函数能够用于更广泛的类型。
通过将函数参数设为对const的引用,我们保证了函数可以用于不能拷贝的类型。大多数类型都允许拷贝,但是也有一些类型不允许拷贝。通过使用引用参数,我们保证了这些类型可以用我们的compareValues函数。而且,如果compareValues被用于处理大对象,这种设计还能让函数运行得更快。
你可能认为更自然的做法是同时使用<和>运算符:
|// 看起来更自然但限制更多的实现 if (first < second) return -1; if (first > second) return 1; return 0;
但是,通过只使用<运算符,我们减少了对compareValues函数所使用类型的要求。这些类型必须支持<,但它们不必也支持>。
实际上,如果我们真的关心类型无关性和可移植性,我们可能应该使用标准库的less函数对象:
|template <typename T> int compareValues(const T &first, const T &second) { if (std::less<T>()(first, second)) return -1; if (std::less<T>()(second, first)) return 1; return 0
当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定实例时,编译器才会生成代码。这一特性影响了我们如何组织源代码以及错误何时被检测到。
通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似地,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。
模板则不同。为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。
模板程序应该尽量减少对实参类型的要求。我们的compareValues函数很好地说明了这一点。我们本来可以编写这个函数,要求参数类型必须支持两个比较操作,但实际上我们只要求支持<运算符。
类模板是生成类的蓝图。与函数模板不同的是,编译器不能为类模板推断模板参数类型。相反,我们必须在模板名后的尖括号中提供额外信息,即模板实参列表。
作为例子,我们将实现一个模板版本的数据容器类。我们把这个模板命名为Container,表明它不再是针对特定类型的。与函数模板一样,类模板以关键字template开始,后跟模板参数列表。在类模板的定义中,我们使用模板参数作为待使用类型或值的占位符:
|template <typename T> class Container { public: typedef T value_type; typedef typename std::vector<T>::size_type size_type; // 构造函数 Container(); Container(std::initializer_list<T> initList); // 容器操作 size_type size
我们的Container模板有一个模板类型参数,名为T。我们在任何需要引用元素类型的地方使用类型参数T。例如,我们将提供元素访问的操作的返回类型定义为T&。当用户实例化Container时,这些T的使用都将被指定的模板实参类型替换。
当使用一个类模板时,我们必须提供显式模板实参,编译器使用这些模板实参来实例化出特定的类:
|Container<int> intContainer; // 空的Container<int> Container<std::string> stringContainer = {"hello", "world", "example"};
intContainer和stringContainer使用相同的类型特定版本的Container(即Container<int>)。从这些定义,编译器会实例化出等价于:
|template <> class Container<int> { public: typedef int value_type; typedef typename std::vector<int>::size_type size_type; Container(); Container(std::initializer_list<int> initList); // ...其他成员 private: std
类模板的成员函数本身是普通函数。但是,类模板的每个实例都有其自己版本的成员。因此,类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数就必须以template开始,后接类模板参数列表。
|template <typename T> void Container<T>::check(size_type index, const std::string &message) const { if (index >= data->size()) throw std::out_of_range(message); } template <typename T>
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。这个特性让我们可以实例化一个类,即使某些成员对于模板参数类型来说是无效的:
|Container<int> intNumbers = {0, 1, 2, 3, 4, 5}; for (size_t i = 0; i != intNumbers.size(); ++i) { intNumbers[i] = i * i; // 实例化Container<int>::operator[] }
这段代码实例化了Container<int>类和它的三个成员函数:operator[]、size和初始化列表构造函数。如果一个成员函数没有被使用,它就不会被实例化。成员函数只有在被用到时才进行实例化这一特性,使得即使某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类。
为了阅读模板类代码,记住一个类模板的名字不是一个类型名可能会有帮助。类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。
但是有一个例外:在类模板自己的作用域中,我们可以直接使用模板名而不提供实参:
|template <typename T> class ContainerIterator { public: ContainerIterator(): current(0) { } ContainerIterator(Container<T> &container, size_t position = 0): containerPtr(container.data), current(position) { } T& operator*() const
你可能会注意到前置递增和递减成员的ContainerIterator返回ContainerIterator&,而不是ContainerIterator<T>&。当我们处于一个类模板的作用域中时,编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样。
模板参数遵循普通的作用域规则。一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。 与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字。但是,与大多数其他上下文不同,在模板内不能重用模板参数名:
|typedef double DataType; template <typename DataType, typename CompareType> void processData(DataType data, CompareType comparator) { DataType temp = data; // temp的类型是模板参数DataType,不是double double CompareType; // 错误:重声明模板参数CompareType }
由于参数名不能重用,所以在一个模板参数列表中,一个模板参数名只能出现一次。
假设T是一个模板类型参数,当编译器遇到类似T::size_type的代码时,它不知道在实例化之前size_type是一个类型还是一个静态数据成员。但是,为了处理模板,编译器必须知道名字是否表示一个类型。
默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们通过使用关键字typename来实现这一点:
|template <typename T> typename T::value_type getLastElement(const T& container) { if (!container.empty()) return container.back(); else return typename T::value_type(); // 返回值初始化的元素 }
当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class。
就像我们能为函数参数提供默认实参一样,我们也可以提供默认模板实参。在新标准中,我们可以为函数和类模板提供默认实参。较早的C++标准只允许为类模板提供默认实参。
作为一个例子,我们重写compareValues,默认使用标准库的less函数对象模板:
|template <typename T, typename Comparator = std::less<T>> int compareValues(const T &first, const T &second, Comparator comp = Comparator()) { if (comp(first, second)) return -1; if (comp(second, first)) return
这里我们为模板添加了第二个类型参数,名为Comparator,它表示可调用对象的类型,并定义了一个新的函数参数comp,它将绑定到一个可调用对象。
我们也为这个模板参数和其对应的函数参数提供了默认值。默认模板实参指出compareValues将使用标准库的less函数对象类,实例化为与compareValues相同的类型参数。默认函数实参表示comp将是类型Comparator的一个默认初始化的对象。
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板。成员模板不能是虚函数。
作为普通类包含成员模板的例子,我们定义一个类似于unique_ptr所使用的默认删除器类型的类。与默认删除器一样,我们的类将有一个重载的函数调用运算符, 它接受一个指针并对给定指针执行delete。与默认删除器不同,我们的类还将在删除器执行时打印一条信息:
|class DebugDelete { public: DebugDelete(std::ostream &stream = std::cerr): outputStream(stream) { } // 与任何函数模板一样,T的类型由编译器推断 template <typename T> void operator()(T *ptr) const { outputStream << "deleting unique_ptr" << std
与任何其他模板一样,成员模板以模板参数列表开始。每个DebugDelete对象都有一个可以写入的ostream成员,以及一个本身是模板的成员函数。我们可以这样使用这个类:
|double* numPtr = new double; DebugDelete debugDeleter; debugDeleter(numPtr); // 调用DebugDelete::operator()(double*),删除numPtr int* intPtr = new int; DebugDelete()(intPtr); // 在一个临时DebugDelete对象上调用operator()(int*)
我们也可以为类模板定义成员模板。在这种情况下,类和成员各自有它们自己的、独立的模板参数。
例如,我们将为Container类定义一个构造函数,它接受两个迭代器,表示要拷贝的元素范围。由于我们希望支持不同类型序列的迭代器,我们将构造函数定义为模板:
|template <typename T> class Container { template <typename Iterator> Container(Iterator begin, Iterator end); // 其他成员 };
这个构造函数有它自己的模板类型参数Iterator,用于其两个函数参数的类型。
在类模板外定义成员模板时,我们必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表:
|template <typename T> // 类的类型参数 template <typename Iterator> // 构造函数的类型参数 Container<T>::Container(Iterator begin, Iterator end): data(std::make_shared<std::vector<T>>(begin, end)) { }
在大多数情况下,调用函数模板时编译器会为我们推断模板实参。编译器从函数实参来确定模板实参的过程被称为模板参数推导。 在模板参数推导过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定的函数调用最为匹配。
与往常一样,顶层const无论是在形参中还是在实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括:
其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。
考虑对函数processData和processRef的调用。processData拷贝它的参数,而processRef的参数是引用:
|template <typename T> T processData(T data1, T data2); template <typename T> T processRef(const T& data1, const T& data2); std::string str1("a value"); const std::string
在第一对调用中,我们传递一个string和一个const string。虽然这些类型不严格匹配,但两个调用都是合法的。
在processData调用中,实参被拷贝,所以原对象是否是const的无关紧要。在processRef调用中,参数类型是const的引用。对于引用参数来说,转换为const是允许的转换,所以这个调用是合法的。
某些情况下,编译器无法推断出模板实参的类型。其他一些情况下,我们希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。
作为第一种情况的一个例子,我们可能希望定义一个类似make_shared的函数,但返回普通指针而不是shared_ptr:
|template <typename T1, typename T2, typename T3> T1 createValue(T2 arg1, T3 arg2);
在这种情况下,没有任何函数实参的类型可用来推断T1的类型。每次调用createValue时,调用者都必须为T1提供一个显式模板实参。
我们提供显式模板实参的方式与定义类模板实例的方式相同。显式模板实参在尖括号中给出,位于函数名之后、实参列表之前:
|// T1是显式指定的;T2和T3是从函数实参类型推断而来的 auto result = createValue<long long>(i, lng); // long long createValue(int, long)
显式模板实参按由左至右的顺序与对应的模板参数匹配;只有尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。
在有些情况下,我们希望用户指定返回类型,但在其他情况下,让编译器找到返回类型更方便。想象编写一个函数,它接受表示序列的一对迭代器和返回序列中一个元素的引用:
|template <typename Iterator> ??? &getElement(Iterator begin, Iterator end) { // 处理序列 return *begin; // 返回序列中一个元素的引用 }
我们不知道返回结果的确切类型,但知道所需的类型是所处理的序列的元素类型。我们知道函数应该返回*begin,我们知道可以用decltype(*begin)来获得此表达式的类型。但是,在编译器遇到函数的参数列表之前,begin都是不存在的。为了定义这个函数,我们必须使用尾置返回类型。因为尾置返回出现在参数列表之后,它可以使用函数的参数:
|template <typename Iterator> auto getElement(Iterator begin, Iterator end) -> decltype(*begin) { // 处理序列 return *begin; // 返回序列中一个元素的引用 }
这里我们通知编译器getElement的返回类型与解引用begin参数的结果类型相同。解引用运算符返回一个左值,所以通过decltype推断的类型为begin所指元素的类型的引用。
当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。例如,假定我们有一个函数指针,它指向的函数返回int、接受两个const引用参数:
|template <typename T> int compareValues(const T&, const T&); // funcPtr1指向实例int compareValues(const int&, const int&) int (*funcPtr1)(const int&, const int&) = compareValues;
funcPtr1中参数的类型决定了T的模板实参的类型。在这个例子中,T被推断为int。指针funcPtr1指向compareValues的int实例化版本。
当存在重载的函数模板时,从函数指针类型不能确定一个唯一的模板实例化版本:
|// 重载的函数版本,每个都接受不同的函数指针类型 void processFunction(int(*)(const std::string&, const std::string&)); void processFunction(int(*)(const int&, const int&)); processFunction(compareValues); // 错误:哪个compareValues实例?
我们可以通过使用显式模板实参来消除歧义:
|processFunction(compareValues<int>); // 正确:显式指定compareValues(const int&, const int&)

函数模板可以被另一个模板或一个普通非模板函数重载。与往常一样,名字相同的函数必须在参数数量或参数类型上有所不同。
如果涉及函数模板,函数匹配规则会以下面的方式受到影响:对于一个调用,其候选函数包括所有模板实参推导成功的函数模板实例。 候选的函数模板总是可行的,因为模板实参推导会排除任何不可行的模板。与往常一样,可行函数按类型转换来排序。当然,可以用于函数模板调用的类型转换是非常有限的。
与往常一样,如果恰好有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是,如果有多个函数提供同样好的匹配,则:如果同样好的函数中只有一个是非模板函数,则选择此函数。 如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。否则,此调用有歧义。
作为一个例子,我们构建一组可能在调试中有用的函数。我们将这些调试函数命名为debug_rep,每个函数都返回一个给定对象的string表示。我们从编写最通用的版本开始,它将一个const对象的引用作为参数:
|// 打印任何我们不能处理的类型 template <typename T> std::string debug_rep(const T &obj) { std::ostringstream ret; ret << obj; // 使用T的输出运算符打印obj的表示 return ret.str(); // 返回ret绑定的string的副本 }
这个函数可以用来生成任何定义了输出运算符的类型的对象对应的string。
接下来,我们定义打印指针的debug_rep版本:
|// 打印指针的值,后跟指针指向的对象 template <typename T> std::string debug_rep(T *ptr) { std::ostringstream ret; ret << "指针: " << ptr; // 打印指针本身的值 if (ptr) ret << " " << debug_rep(*ptr); // 打印ptr指向的值 else ret << " 空指针"; return
现在考虑这些函数可能如何使用:
|std::string message("hello"); std::cout << debug_rep(message) << std::endl;
对于这个调用,只有第一个版本的debug_rep是可行的。第二个版本的debug_rep要求一个指针参数,而在此调用中我们传递了一个非指针对象。
如果我们用一个指针调用debug_rep:
|std::cout << debug_rep(&message) << std::endl;
这次两个函数都生成可行的实例化版本。第二个版本的debug_rep的实例化版本需要进行精确匹配。第一个版本的实例化版本需要从普通指针到指向const的指针的转换。正常的函数匹配规则告诉我们应该选择第二个模板,而且的确是如此。
作为另一个例子,考虑下面的调用:
|const std::string *strPtr = &message; std::cout << debug_rep(strPtr) << std::endl;
这里两个模板都是可行的,而且两个都提供精确匹配:debug_rep(const string*&),它是第一个模板的实例化,其中T被绑定到const string*;debug_rep(const string*),它是第二个模板的实例化,其中T被绑定到const string。
正常函数匹配无法区分这两个函数。我们可能希望这个调用是有歧义的。但是,根据重载函数模板的特殊规则,此调用被解析为debug_rep(T*),这是更特例化的模板。
这条规则的原因是,如果没有它,将无法对指向const的指针调用指针版本的debug_rep。问题在于模板debug_rep(const T&)本质上可以用于任何类型,包括指针类型。此模板比debug_rep(T*)更通用,后者只能用于指针类型。
作为下一个例子,我们定义一个普通非模板版本的debug_rep来打印双引号包围的string:
|// 打印双引号包围的string std::string debug_rep(const std::string &str) { return '"' + str + '"'; }
现在,当我们对一个string调用debug_rep时:
|std::string message("hello"); std::cout << debug_rep(message) << std::endl;
有两个同样好的可行函数:debug_rep<string>(const string&),第一个模板,T被绑定到string;debug_rep(const string&),普通的非模板函数。
在这种情况下,两个函数具有相同的参数列表,所以显然每个函数都为此调用提供同样好的匹配。但是,选择非模板版本。对于一个调用,如果一个非模板函数与一个函数模板提供同样好的匹配,则选择非模板版本。
可变参数模板是能够接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个函数参数。
我们用一个省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,class...或typename...指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。例如:
|// Args是一个模板参数包;rest是一个函数参数包 // Args表示零个或多个模板类型参数 // rest表示零个或多个函数参数 template <typename T, typename... Args> void processItems(const T &first, const Args& ... rest);
这里我们声明了processItems是一个可变参数函数,它有一个名为T的类型参数,和一个名为Args的模板参数包。这个包表示零个或多个额外的类型参数。函数的参数列表包含一个const &类型的参数,指向T的类型,还有一个名为rest的函数参数包,此包表示零个或多个函数参数。
与往常一样,编译器从函数的实参推断模板参数类型。对于一个可变参数模板,编译器还会推断包中参数的数目。例如:
|int i = 0; double d = 3.14; std::string s = "how now brown cow"; processItems(i, s, 42, d); // 包中有三个参数 processItems(s, 42, "hi"); // 包中有两个参数 processItems(d, s); // 包中有一个参数 processItems("hi"); // 空包
编译器会实例化出四个不同的processItems实例。
当我们需要知道一个包中有多少元素时,可以使用sizeof...运算符。类似sizeof,sizeof...也返回一个常量表达式,而且不会对其实参求值:
|template<typename ... Args> void displayCount(Args ... args) { std::cout << sizeof...(Args) << std::endl; // 类型参数的数目 std::cout << sizeof...(args) << std::endl; // 函数参数的数目 }
可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。为了终止递归,我们还需要定义一个非可变参数的函数。
例如,我们定义一个函数,可以打印任何数目、任何类型的实参:
|// 用来终止递归并打印最后一个元素的函数 // 此函数必须在可变参数版本的print定义之前声明 template<typename T> std::ostream &print(std::ostream &output, const T &t) { return output << t; // 包中最后一个元素后面不打印分隔符 } // 包中除了最后一个元素之外的其他元素都会调用这个版本的print template <typename T, typename... Args>
可变参数版本的print函数接受三个参数:一个ostream&、一个const T&和一个参数包。这个函数打印绑定到t的实参,然后调用自身来打印函数参数包中的剩余值。
关键部分是可变参数函数中对print的调用:
|return print(output, rest...); // 递归调用,打印其他实参
可变参数版本的print函数接受三个参数,但这个调用只传递了两个实参。其中第一个实参是rest中的第一个实参,剩余实参构成下一个print调用的参数包。因此,在每个调用中,包中的第一个实参被移除,成为绑定到t的实参。
对于非可变参数版本与可变参数版本的print的最后调用,两个版本提供同样好的匹配。但是,非可变参数模板比可变参数模板更特例化,因此编译器选择非可变参数版本。
有时,通用模板的定义对特定类型不合适:通用定义可能编译失败或做错事情。在其他时候,我们可以利用一些特定的知识来编写更高效的代码,而不是从模板实例化出的代码。当我们不能(或不想)使用模板版本时,可以定义类或函数模板的一个特例化版本。
我们的compareValues函数就是一个很好的例子,说明了通用定义对特定类型不合适。对于字符指针,我们希望compareValues通过调用strcmp而不是比较指针值来比较字符串。
当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应该使用关键字template后跟一个空尖括号对(<>)。空尖括号指出我们将为原模板的所有模板参数提供实参:
|// compareValues的特例化版本,用于处理字符数组的指针 template <> int compareValues(const char* const &p1, const char* const &p2) { return strcmp(p1, p2); }
理解这个特例化的难点是函数参数类型。当我们特例化一个模板时,函数参数类型必须与一个先前声明的模板中对应的类型匹配。这里我们正在特例化:
|template <typename T> int compareValues(const T&, const T&);
其中函数参数是const类型的引用。我们希望定义此函数的一个特例化版本,其中T为const char*。我们的函数要求一个const指针的引用。
特例化的本质是实例化一个模板,而非重载它。因此,特例化不影响函数匹配。
除了特例化函数模板,我们还可以特例化类模板。作为一个例子,我们为标准库hash模板定义一个特例化版本,使得可以将自定义类型对象保存在无序容器中。
一个特例化hash类必须定义:一个重载的调用运算符,它返回size_t并接受一个容器关键字类型的对象;两个类型成员,result_type和argument_type,分别调用运算符的返回类型和参数类型;默认构造函数和拷贝赋值运算符(可以隐式定义)。
唯一的复杂性在于我们必须在原模板所属的命名空间中特例化它。我们可以向命名空间添加成员。为了做到这一点,我们必须先打开命名空间:
|// 打开std命名空间,以便特例化std::hash namespace std { template <> // 我们正在定义一个特例化版本 struct hash<DataRecord> { // 模板参数为DataRecord typedef size_t result_type; typedef DataRecord argument_type; // 默认情况下,此类型需要== size_t operator()(const DataRecord& record) const; }; size_t hash<DataRecord>::operator()(
与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非全部模板参数,或是参数的一部分而非全部特性。一个类模板的部分特例化本身是一个模板。用户必须为那些在特例化版本中未指定的模板参数提供实参。
我们只能部分特例化类模板,而不能部分特例化函数模板。
|// 原始的、最通用的版本 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&&> // 右值引用
第一个模板定义了最通用的版本。它可以用任何类型实例化;它使用模板实参作为其成员的类型。后面两个类是原模板的部分特例化版本。
由于部分特例化版本是模板,所以我们像往常一样以定义模板参数开始。一个部分特例化版本的模板参数列表是原始模板的参数列表的一个子集或者是一个特例化版本。
部分特例化版本的模板参数列表不能与原始模板的参数列表相同(否则就不是特例化,而是原模板的重新声明了)。
现在让我们通过一些简单的练习题来巩固本章学到的知识。每道题都涵盖了模板的核心概念,请先尝试独立完成,然后再查看答案。
设计一个函数模板findMax,找出数组中的最大元素。要求能处理任意类型的数组,使用模板类型参数和非类型参数。
|#include <iostream> #include <algorithm> using namespace std; // 函数模板:找出数组中的最大元素 template<typename T, size_t N> T findMax(const T (&array)[N]) { T max_val = array[0]; for (size_t i = 1; i < N; i++
实现一个简单的Pair类模板,可以存储两个不同类型的值。
|#include <iostream> #include <string> using namespace std; // 类模板:存储一对值 template<typename T1, typename T2> class Pair { private: T1 first_; T2 second_; public: Pair(const T1& first, const T2& second) :
为下面的serialize函数模板提供string类型的特例化版本。
|template<typename T> string serialize(const T& obj) { ostringstream oss; oss << obj; return oss.str(); }
|#include <iostream> #include <sstream> #include <string> using namespace std; // 通用模板:将对象转换为字符串 template<typename T> string serialize(const T& obj) { ostringstream oss; oss << obj; return oss.str(); } // 特例化:string类型直接返回 template<>
实现一个函数模板print,可以打印任意数量和类型的参数。
|#include <iostream> using namespace std; // 基础情况:没有参数时什么都不做 void print() { cout << endl; } // 递归情况:打印第一个参数,然后递归处理剩余参数 template<typename T, typename... Args> void print(const T& first, const Args&... rest) { cout
实现一个简单的Stack类模板,支持基本的栈操作(push、pop、top)。
|#include <iostream> #include <stdexcept> using namespace std; template<typename T> class Stack { private: static const size_t MAX_SIZE = 100; T data_[MAX_SIZE]; size_t top_; public: Stack() : top_(0) {} void
这个函数模板使用了类型参数T和非类型参数N。T表示数组元素的类型,N表示数组的大小。使用引用传递数组可以避免数组退化为指针,同时保留数组大小信息。编译器会根据实际调用的参数自动推导出T和N的值。
类模板允许我们定义一个通用的类,可以处理不同的数据类型。在创建对象时,需要显式指定模板参数(如Pair<int, string>)。类模板的成员函数定义通常也放在头文件中,因为它们也是模板。
模板特例化允许我们为特定类型提供特殊的实现。当调用serialize时,编译器会优先选择最匹配的版本。对于string类型,直接返回字符串比通过ostringstream转换更高效;对于bool类型,返回"true"/"false"比"1"/"0"更直观。
可变参数模板使用...语法,可以接受任意数量和类型的参数。这个实现使用了递归展开:每次处理一个参数,然后递归处理剩余参数。sizeof...(rest)可以获取参数包中参数的数量。可变参数模板是C++11引入的强大特性,让函数更加灵活。
这个例子展示了类模板的实际应用。Stack类模板可以存储任意类型的元素,通过模板参数T指定元素类型。我们创建了Stack<int>和Stack<string>两个不同的类型,它们有相同的逻辑但处理不同的数据类型。这就是模板的强大之处:一次编写,适用于多种类型。