可以定义一个通用的函数模板,而不是为每个类型都定义一个新函数。
一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。
compare 的模板版本可能像下面这样:
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
模板定义以关键字 template 开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用小于号(<)和大于号(>)包围起来。
当使用模板时,我们(隐式地或显式地)指定模板实参,将其绑定到模板参数上。
compare 函数声明了一个名为 T 的类型参数。在 compare 中,我们用名字 T 表示一个类型。而 T 表示的实际类型则在编译时根据 compare 的使用情况来确定。
实例化函数模板
当调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参。
编译器用推断出的模板参数来为我们实例化一个特定版本的函数。
当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例”。
模板类型参数
可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型换行:
// 正确:返回类型和参数类型相同
template <typename T> T foo(T* p)
{
T tmp = *p; // tmp 的类型将是指针p指向的类型
// ...
return tmp;
}
非类型模板参数
还可以在模板中定义非类型参数。
一个非类型参数表示一个值而非一个类型。通过一个特定的类型名而非关键字 class 或 typename 来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。
一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。
绑定到非类型整型参数的实参必须是一个常量表达式。
绑定到指针或引用非类型参数的实参必须具有静态的生存期。
在模板定义内,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如,指定数组大小。
inline 和 constexpr 的函数模板
函数模板可以声明为 inline 或 constexpr 的,如同非模板函数一样。
inline 或 constexpr 说明符放在模板参数列表之后,返回类型之前。
函数模板和类模板成员函数的定义通常放在头文件中。
通常编译器会在三个阶段报告错误。
类模板是用来生成类的蓝图的。
为了使用类模板,必须在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。
定义类模板
类模板以关键字 template 开始,后跟模板参数列表。
在类模板(及其成员)的定义中,我们将模板参数当作替身,代替使用模板时用户需要提供的类型或值。
template <typename T> class <类名>
{
<类成员声明>
};
实例化类模板
使用类模板时,必须提供额外信息,这些额外信息是显式模板实参列表,它们绑定到模板参数。
一个模板参数的名字也没有什么内在含义。
通常将类型参数命名为 T,但实际上可以使用任何名字。
template <typename Foo> Foo calc(const Foo&, const Foo& b)
{
Foo tmp = a; // tmp 的类型与参数和返回类型一样
// ...
return tmp; // 返回类型和参数类型一样
}
模板参数与作用域
模板参数遵循普通的作用域规则。
一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。
在模板内不能重用模板参数名:
typedef double A;
template <typename A, typename B> void f(A a, B b)
{
A tmp = a; // tmp 的类型为模板参数A的类型,而非 double
double B; // 错误:重声明模板参数B
}
模板声明
模板声明必须包含模板参数。
声明中的模板参数的名字不必与定义中相同。
一个给定模板的每个声明和定义必须由相同数量和种类(即,类型或非类型)的参数。
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。
这种成员被称为成员模板。成员模板不能是虚函数。
普通(非模板)类的成员模板
与任何其他模板相同,成员模板也是以模板参数列表开始的。
类模板的成员模板
对于类模板,也可以为其定义成员模板。在此情况下,类和成员各自有自己的、独立的模板参数。
与类模板的普通函数成员不同,成员模板是函数模板。
当在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。
类模板的参数列表在前,后跟成员自己的模板参数列表。
当模板被使用时才会进行实例化。意味着,相同的实例可能出现在多个对象文件中。
当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就会有该模板的一个实例。
可以通过显式实例化来避免实例化相同模板的额外开销。
一个显式实例化有如下形式:
extern template declaration; // 实例化声明
template declaration; // 实例化定义
declaration 是一个类或函数声明,其中所有模板参数已被替换为模板实参。
从函数实参来确定模板实参的过程被称为模板实参推断。
编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。
在其他类型转换中,能在调用中应用于函数模板的包括如下两项。
其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。
使用相同模板参数类型的函数形参
一个模板类型参数可以用作对各函数形参的类型。
由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。
如果推断出的类型不匹配,则调用就是错误的。
正常类型转换应用于普通函数实参
函数模板可以有用普通类型定义的参数,即,不涉及模板类型参数的类型。
这个函数实参不进行特殊处理:它们正常转换为对应形参的类型。
指定显式模板实参
// 编译器无法推断T1,它未出现在函数参数列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
每次调用 sum 时调用者都必须为 T1 提供一个显式模板实参。
显式模板实参在尖括号中给出,位于函数名之后,实参列表之前:
// T1 是显式指定的,T2和T3是从函数实参类型推断而来的
auto val3 = sum<long long>(i, lng); // long long sum(int, long)
显式模板实参按由左至右的顺序与对应的模板参数匹配;第一个模板参数与第一个模板参数匹配,第二个实参与第二个参数匹配,依此类推。
只有尾部(最右)参数的显示模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。
正常类型转换应用于显示指定的实参
对于用普通类型定义的函数参数,允许进行正常的类型转换,出于同样的原因,对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换。
long lng;
compare(lng, 1024); // 错误:模板参数不匹配
compare<long>(lng, 1024); // 正确:实例化 compare(long, long)
compare<int>(lng, 1024); // 正确:实例化 compare(int, int)
当用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。
学习参考资料:
C++ 中文版 Primer (第5版)