C++ Function Templates (函数模板)

1. Templates and Generic Programming (模板与泛型编程)

Both object-oriented programming (OOP) and generic programming deal with types that are not known at the time the program is written. The distinction between the two is that OOP deals with types that are not known until run time, whereas in generic programming the types become known during compilation.
面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于,面向对象编程能处理类型在程序运行之前都未知的情况;而在泛型编程中,在编译时就能获知类型了。

When we write a generic program, we write the code in a way that is independent of any particular type. When we use a generic program, we supply the type(s) or value(s) on which that instance of the program will operate.
当我们编写一个泛型程序时,是独立于任何特定类型来编写代码的。当使用一个泛型程序时,我们提供类型或值,程序实例可在其上运行。容器、迭代器等都是泛型编程的例子。

For example, the library provides a single, generic definition of each container, such as vector. We can use that generic definition to define many different types of vectors, each of which differs from the others as to the type of elements the vector contains.
例如,标准库为每个容器提供了单一的、泛型的定义,如 vector,我们可以使用这个泛型定义来定义很多类型的 vector,它们的差异就在于包含的元素类型不同。

Templates are the foundation of generic programming. Templates are the foundation for generic programming in C++. A template is a blueprint or formula for creating classes or functions.
模板是泛型编程的基础。模板是 C++ 中泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。

That transformation happens during compilation.
当使用一个 vector 这样的泛型类型,或者 find 这样的泛型函数肘,我们提供足够的信息,将蓝图转换为特定的类或函数。这种转换发生在编译时。

2. Defining a Function Templates (定义函数模板)

函数模板是通用的函数描述,它们使用泛型来定义函数,其中的泛型可用具体的类型替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型 (而不是具体类型) 的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型 (parameterized types)。

函数模板允许以任意类型的方式来定义函数。可以定义一个通用的函数模板 (function template),而不是为每个类型都定义一 个新函数。一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。

template <typename AnyType>
void swap_func(AnyType &a, AnyType &b) {
	AnyType temp;
	temp = a;
	a = b;
	b = temp;
}


在标准 C++ 98 添加关键字 typename 之前,C++ 使用关键字 class 来创建模板。可以这样编写模板定义:

template <class AnyType>
void swap_func(AnyType &a, AnyType &b) {
	AnyType temp;
	temp = a;
	a = b;
	b = temp;
}

要建立一个模板,并将类型命名为 AnyType。关键字 templatetypename 是必需的,除非可以使用关键字 class 代替 typename,必须使用尖括号。类型名可以任意选择 (AnyType),只要遵守 C++ 命名规则即可。许多程序员都使用简单的名称,如 T

模板定义以关键字 template 开始,后跟一个模板参数列表 (template parameter list),这是一个逗号分隔的一个或多个模板参数 (template parameter) 的列表,用小于号 (<) 和大于号 (>) 包围起来。

在模板定义中,模板参数列表不能为空。

模板参数列表的作用很像函数参数列表。函数参数列表定义了若干特定类型的局部变量,但并未指出如何初始化它们。在运行时,调用者提供实参来初始化形参。

模板参数表示在类或函数定义中用到的类型或值。当使用模板时,我们 (隐式地或显式地) 指定模板实参 (template argument),将其绑定到模板参数上。

模板并不创建任何函数,而只是告诉编译器如何定义函数。需要交换 int 的函数时, 编译器将按模板模式创建这样的函数,并用 int 代替 AnyType。同样,需要交换 double 的函数时,编译器将按模板模式创建这样的函数,并用 double 代替 AnyType。

typename 关键字使得参数 AnyType 表示类型这一点更为明显,然而有大量代码库是使用关键字 class 开发的。

如果需要多个将同一种算法用于不同类型的函数,请使用模板。如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字 typename 而不使用 class

要让编译器知道程序需要一个特定形式的交换函数,只需在程序中使用 swap_func() 函数即可。编译器将检查所使用的参数类型,并生成相应的函数。

在文件的开始位置提供模板函数的原型,并在 main() 函数后面提供模板函数的定义。更常见的做法, 即将 T 而不是 AnyType 用作类型参数。

// using a function template

#include <iostream>

// function template prototype
template <typename T>  // or class T
void swap_func(T &a, T &b);

int main() {
	using namespace std;

	int i = 10;
	int j = 20;
	cout << "i, j = " << i << ", " << j << ".\n";
	cout << "Using compiler-generated int swapper:\n";
	swap_func(i, j);  // generates void swap_func(int &, int &)
	cout << "Now i, j = " << i << ", " << j << ".\n";

	double x = 24.5;
	double y = 81.7;
	cout << "x, y = " << x << ", " << y << ".\n";
	cout << "Using compiler-generated double swapper:\n";
	swap_func(x, y);  // generates void swap_func(double &, double &)
	cout << "Now x, y = " << x << ", " << y << ".\n";
	// cin.get();
	return 0;
}

// function template definition
template <typename T>  // or class T
void swap_func(T &a, T &b) {
	T temp;  // temp a variable of type T
	temp = a;
	a = b;
	b = temp;
}

swap_func() 函数声明了一个名为 T 的类型参数。在 swap_func() 函数中,我们用名字 T 表示一个类型。T 表示的实际类型则在编译时根据 swap_func() 函数的使用情况来确定。

第一个 swap_func() 函数接受两个 int 参数,因此编译器生成该函数的 int 版本。用 int 替换所有的 T,生成下面这样的定义:

void swap_func(int &a, int &b) {
	int temp;  // temp a variable of type int
	temp = a;
	a = b;
	b = temp;
}

程序员看不到这些代码,但编译器确实生成并在程序中使用了它们。第二个 swap_func() 函数接受两个 double 参数,因此编译器将生成 double 版本。用 double 替换 T,生成下述代码:

void swap_func(double &a, double &b) {
	double temp;  // temp a variable of type double
	temp = a;
	a = b;
	b = temp;
}

函数模板不能缩短可执行程序。上述程序最终仍将由两个独立的函数定义,就像以手工方式定义了这些函数一样。最终的代码不包含任何模板,而只包含了为程序生成的实际函数。使用模板的好处是,它使生成多个函数定义更简单、更可靠。更常见的情形是,将模板放在头文件中,并在需要使用模板的文件中包含头文件。

i, j = 10, 20.
Using compiler-generated int swapper:
Now i, j = 20, 10.
x, y = 24.5, 81.7.
Using compiler-generated double swapper:
Now x, y = 81.7, 24.5.
请按任意键继续. . .

2.1. Instantiating a Function Template (实例化函数模板)

当我们调用一个函数模板时,编译器 (通常) 用函数实参来为我们推断模板实参。即,当我们调用 swap_func() 时,编译器使用实参的类型来确定绑定到模板参数 T 的类型。

在下面的调用中:

swap_func(i, j);  // generates void swap_func(int &, int &)

实参类型是 int,编译器会推断出模板实参为 int,并将它绑定到模板参数 T

编译器用推断出的模板参数来为我们实例化 (instantiate) 一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新实例。

// 实例化出 void swap_func(int &, int &)
swap_func(i, j);  // generates void swap_func(int &, int &)
// 实例化出 void swap_func(double &, double &)
swap_func(x, y);  // generates void swap_func(double &, double &)

编译器会实例化出两个不同版本的 swap_func()。对于第一个调用,编译器会编写并编译一个 swap_func() 版本,其中 T 被替换为 int

void swap_func(int &a, int &b) {
	int temp;  // temp a variable of type int
	temp = a;
	a = b;
	b = temp;
}

对于第二个调用,编译器会生成另一个 swap_func() 版本,其中 T 被替换为 double。这些编译器生成的版本通常被称为模板的实例 (instantiation)

void swap_func(double &a, double &b) {
	double temp;  // temp a variable of type double
	temp = a;
	a = b;
	b = temp;
}

2.2. Template Type Parameters (模板类型参数)

swap_func() 函数有一个模板类型参数 (type parameter)。一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。

// 正确:返回类型和参数类型相同
template <typename T>
T foo(T *p) {
	T tmp = *p;  // tmp 的类型将是指针 p 指向的类型
	// ...
	return tmp;
}

类型参数前必须使用关键字 classtypename,在模板参数列表中,这两个关键字的含义相同,可以互换使用。

// 错误:U 之前必须加上 class 或 typename
template <typename T, U>
T calc(const T &, const U &);

一个模板参数列表中可以同时使用这两个关键字,关键字 typename 来指定模板类型参数比用 class 更为直观。可以用内置 (非类) 类型作为模板类型实参。而且,typename 更清楚地指出随后的名字是一个类型名。但是,typename 是在模板已经广泛使用之后才引入 C++ 语言的,某些程序员仍然只用 class

// 正确:在模板参数列表中,typename 和 class 相同
template <typename T, class U>
T calc(const T &, const U &);

2.3. Nontype Template Parameters (非类型模板参数)

除了定义类型参数,还可以在模板中定义非类型参数 (nontype parameter)。一个非类型参数表示一个值而非一个类型,我们通过一个特定的类型名而非关键字 classtypename 来指定非类型参数。

当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。

我们可以编写一个 compare() 函数处理字符串字面常量,这种字面常量是 const char 的数组。由于不能拷贝一个数组,所以我们将自己的参数定义为数组的引用。由于我们希望能比较不同长度的字符串字面常量,因此为模板定义了两个非类型的参数。第一个模板参数表示第一个数组的长度,第二个参数表示第二个数组的长度:

template <unsigned int N, unsigned int M>
int compare(const char (&pl)[N], const char (&p2)[M]) {
	return strcmp(pl, p2);
}

当我们调用这个版本的 compare() 函数时:

compare("yong", "qiang");

编译器会使用字面常量的大小来代替 N 和 M,从而实例化模板。编译器会在一个字符串字面常量的末尾插入一个空字符作为终结符,因此编译器会实例化出如下版本:

int compare(const char (&pl)[5], const char (&p2)[6])

一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或 (左值) 引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。不能用一个普通 (非 static) 局部变量或动态对象作为指针或引用非类型模板参数的实参。指针参数也可以用 nullptr 或一个值为 0 的常量表达式来实例化。

在模板定义内,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如指定数组大小。

非类型模板参数的模板实参必须是常量表达式。

2.4. inline and constexpr Function Templates

函数模板可以声明为 inlineconstexpr 的,如同非模板函数一样。inlineconstexpr 说明符放在模板参数列表之后,返回类型之前:

// 正确:inline 说明符跟在模板参数列表之后
template <typename T> inline T min(const T &, const T &);
// 错误:inline 说明符的位置不正确
inline template <typename T> T min(const T &, const T &);

2.5. Template Compilation (模板编译)

当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。当我们使用 (而不是定义) 模板时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到。

通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。

模板则不同。为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。

函数模板和类模板成员函数的定义通常放在头文件中。

模板包含两种名字:

  • 那些不依赖于模板参数的名字
  • 那些依赖于模板参数的名字

当使用模板时,所有不依赖于模板参数的名字都必须是可见的,这是由模板的提供者来保证的。而且,模板的提供者必须保证,当模板被实例化时,模板的定义,包括类模板的成员的定义,也必须是可见的。

用来实例化模板的所有函数、类型以及与类型关联的运算符的声明都必须是可见的,这是由模板的用户来保证的。

通过组织良好的程序结构,恰当使用头文件,这些要求都很容易满足。模板的设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。

  • 大多数编译错误在实例化期间报告

模板直到实例化时才会生成代码,这一特性影响了我们何时才会获知模板内代码的编译错误。通常,编译器会在三个阶段报吿错误。

第一个阶段是编译模板本身时。在这个阶段,编译器通常不会发现很多错误。编译器可以检查语法错误,例如忘记分号或者变量名拼错等。

第二个阶段是编译器遇到模板使用时。在此阶段,编译器仍然没有很多可检查的。对函数模板调用,编译器通常会检查实参数目是否正确。它还能检查参数类型是否匹配。对于类模板,编译器可以检查用户是否提供了正确数目的模板实参。

第三个阶段是模板实例化时,只有这个阶段才能发现类型相关的错误。依赖于编译器如何管理实例化,这类错误可能在链接时才报告。

template <typename T>
int compare(const T &vl, const T &v2) {
	if (vl < v2) return -1;
	if (v2 < vl) return 1;
	return 0;
}

当我们编写模板时,代码不能是针对特定类型的,但模板代码通常对其所使用的类型有一些假设。编写的模板函数很可能无法处理某些类型。

SalesData datal, data2;
cout << compare(datal, data2) << endl;  // 错误:SalesData 未定义 <

此调用实例化了 compare() 的一个版本,将 T 替换为 SalesData。if 条件试图对 SalesData 对象使用 < 运算符,但 SalesData 并未定义此运算符。此实例化生成了一个无法编译通过的函数版本。但是,这样的错误直至编译器在类型 SalesData 上实例化 compare() 时才会被发现。

保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。

  • Writing Type-Independent Code (编写类型无关的代码)

函数参数设定为 const 的引用,我们保证了函数可以用于不能拷贝的类型。大多数类型,包括内置类型和我们已经用过的标准库类型 (除 unique_ptrIO 类型之外),都是允许拷贝的。但是,不允许拷贝的类类型也是存在的。如果 const 的引用应用于大对象,这种设计策略还能使函数运行得更快。

模板程序应该尽量减少对实参类型的要求。

3. Overloaded Templates (重载的模板)

需要多个对不同类型使用同一种算法的函数时,可使用模板。然而,并非所有的类型都使用相同的算法。为满足这种需求,可以像重载常规函数定义那样重载模板定义。和常规重载一样,被重载的模板的函数特征必须不同。

下述程序新增了一个交换模板,用于交换两个数组中的元素。原来的模板的特征为 (T &, T &), 而新模板的特征为 (T [], T [], int)。注意,在后一个模板中,最后一个参数的类型为具体类型 int,而不是泛型。并非所有的模板参数都必须是模板参数类型。

编译器见到第一个 swap_func() 函数调用时,发现它有两个 int 参数,因此将它与原来的模板匹配。但第二次调用将两个 int 数组和一个 int 值用作参数,这与新模板匹配。

// using overloaded template functions

#include <iostream>

template <typename T>  // original template
void swap_func(T &a, T &b);

template <typename T>  // new template
void swap_func(T *a, T *b, int n);

void show(int a[]);

const int NUM = 8;

int main() {
	using namespace std;

	int i = 10, j = 20;
	cout << "i, j = " << i << ", " << j << ".\n";
	cout << "Using compiler-generated int swapper:\n";
	swap_func(i, j);  // matches original template
	cout << "Now i, j = " << i << ", " << j << ".\n";

	int d1[NUM] = { 0, 7, 0, 4, 1, 7, 7, 6 };
	int d2[NUM] = { 0, 7, 2, 0, 1, 9, 6, 9 };
	cout << "Original arrays:\n";
	show(d1);
	show(d2);
	swap_func(d1, d2, NUM);  // matches new template
	cout << "Swapped arrays:\n";
	show(d1);
	show(d2);
	// cin.get();
	return 0;
}

template <typename T>
void swap_func(T &a, T &b) {
	T temp;
	temp = a;
	a = b;
	b = temp;
}

template <typename T>
void swap_func(T a[], T b[], int n) {
	T temp;
	for (int i = 0; i < n; i++) {
		temp = a[i];
		a[i] = b[i];
		b[i] = temp;
	}
}

void show(int a[]) {
	using namespace std;
	
	cout << a[0] << a[1] << "/";
	cout << a[2] << a[3] << "/";
	for (int i = 4; i < NUM; i++) {
		cout << a[i];
	}
	cout << endl;
}

i, j = 10, 20.
Using compiler-generated int swapper:
Now i, j = 20, 10.
Original arrays:
07/04/1776
07/20/1969
Swapped arrays:
07/20/1969
07/04/1776
请按任意键继续. . .

4. Explicit Specializations (显式具体化)

定义如下结构,

struct job {
	char name[40];
	double salary;
	int floor;
};

假设希望能够交换两个这种结构的内容,原来的模板使用下面的代码来完成交换,

	temp = a;
	a = b;
	b = temp;

C++ 允许将一个结构赋给另一个结构,因此即使 T 是一个 job 结构,上述代码也适用。然而,假设只想交换 salary 和 floor 成员,而不交换 name 成员,则需要使用不同的代码,但 swap_func() 的参数将保持不变 (两个 job 结构的引用),因此无法使用模板重载来提供其他的代码。

可以提供一个具体化函数定义称为显式具体化 (explicit specialization),其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。

(1) Third-Generation Specialization (ISO/ANSI C++ Standard)

试验其它具体化方法后,C++ 98 标准选择了下面的方法:

  • For a given function name, you can have a non template function, a template function, and an explicit specialization template function, along with overloaded versions of all of these. 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。
  • The prototype and definition for an explicit specialization should be preceded by template <> and should mention the specialized type by name. 显式具体化的原型和定义应以 template<> 打头,并通过名称来指出类型。
  • A specialization overrides the regular template, and a non template function overrides both. 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。

下面是用于交换 job 结构的非模板函数、模板函数和具体化的原型:

// non template function prototype
void swap_func(job &, job &)

// template prototype
template <typename T>
void swap_func(T &, T &)

// explicit specialization for the job type
template <>
void swap_func<job>(job &, job &)

如果有多个原型,则编译器在选择原型时,非模板版本优先于显式具体化和模板版本,而显式具体化优先于使用模板生成的版本。

例如,在下面的代码中,第一次调用 swap_func() 时使用通用模板函数,而第二次调用使用基于 job 类型的显式具体化版本。

...
template <class T>  // general template
void swap_func(T &, T &);

// explicit specialization for the job type
template <>
void swap_func<job>(job &, job &);

int main() {
	double u, v;
	...
	swap_func(u, v);  // use template
	job a, b;
	...
	swap_func(a, b);  // use void swap_func<job>(job &, job &)
}

(2) An Example of Explicit Specialization (显式具体化示例)

// specialization overrides a template

#include <iostream>

template <typename T>
void swap_func(T &a, T &b);

struct job {
	char name[40];
	double salary;
	int floor;
};

// explicit specialization 
template <>
void swap_func<job>(job &j1, job &j2);

void show(job &j);

int main() {
	using namespace std;

	cout.precision(2);
	cout.setf(ios::fixed, ios::floatfield);
	int i = 10, j = 20;
	cout << "i, j = " << i << ", " << j << ".\n";
	cout << "Using compiler-generated int swapper:\n";
	swap_func(i, j);  // generates void swap_func(int &, int &)
	cout << "Now i, j = " << i << ", " << j << ".\n";

	job sue = { "Susan Yaffee", 73000.60, 7 };
	job sidney = { "Sidney Taffee", 78060.72, 9 };
	cout << "Before job swapping:\n";
	show(sue);
	show(sidney);
	swap_func(sue, sidney);  // uses void swap_func(job &, job &)
	cout << "After job swapping:\n";
	show(sue);
	show(sidney);
	// cin.get();
	return 0;
}

template <typename T>
void swap_func(T &a, T &b)  // general version
{
	T temp;
	temp = a;
	a = b;
	b = temp;
}

// swaps just the salary and floor fields of a job structure
template <>
void swap_func<job>(job &j1, job &j2)  // specialization
{
	double t1;
	int t2;
	t1 = j1.salary;
	j1.salary = j2.salary;
	j2.salary = t1;
	t2 = j1.floor;
	j1.floor = j2.floor;
	j2.floor = t2;
}

void show(job &j) {
	using namespace std;

	cout << j.name << ": $" << j.salary << " on floor " << j.floor << endl;
}

i, j = 10, 20.
Using compiler-generated int swapper:
Now i, j = 20, 10.
Before job swapping:
Susan Yaffee: $73000.60 on floor 7
Sidney Taffee: $78060.72 on floor 9
After job swapping:
Susan Yaffee: $78060.72 on floor 9
Sidney Taffee: $73000.60 on floor 7
请按任意键继续. . .

5. Instantiations and Specializations (实例化和具体化)

在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例 (instantiation)。例如,函数调用 swap_func(i, j); 导致编译器生成 swap_func() 的一个实例,该实例使用 int 类型。模板并非函数定义,但使用 int 的模板实例是函数定义。这种实例化方式被称为隐式实例化 (implicit instantiation),因为编译器之所以知道需要进行定义,是由于程序调用 swap_func() 函数时提供了 int 参数。

除了通过隐式实例化,来使用模板生成函数定义,C++ 还允许显式实例化 (explicit instantiation)。这意味着可以直接命令编译器创建特定的实例, 如 swap_func<int>()。其语法是声明所需的种类 - 用 <> 符号指示类型,并在声明前加上关键字 template

template
void swap_func<int>(int, int);  // explicit instantiation

编译器看到上述声明后,将使用 swap_func() 模板生成一个使用 int 类型的实例。该声明的意思是使用 swap_func() 模板生成 int 类型的函数定义。

显式实例化不同的是,显式具体化使用下面两个等价的声明之一:

template <>
void swap_func<int>(int &, int &);  // explicit specialization

template <>
void swap_func(int &, int &);  // explicit specialization

The difference is that these last two declarations mean “Don’t use the swap_func() template to generate a function definition. Instead, use a separate, specialized function definition explicitly defined for the int type.” These prototypes have to be coupled with their own function definitions.The explicit specialization declaration has <> after the keyword template, whereas the explicit instantiation omits the <>.
显式具体化声明的意思是不要使用 swap_func() 模板来生成函数定义,而应使用专门为 int 类型显式地定义的函数定义。这些原型必须有自己的函数定义。显式具体化声明在关键字 template 后包含 <>,而显式实例化没有。

It is an error to try to use both an explicit instantiation and an explicit specialization for the same type(s) in the same file, or, more generally, the same translation unit.
试图在同一个文件或转换单元中使用同一种类型的显式实例和显式具体化将出错。

在程序中使用函数来创建显式实例化:

template <class T>
T Add(T a, T b)  // pass by value
{
	return a + b;
}
...
int m = 6;
double x = 10.2;
cout << Add<double>(x, m) << endl;  // explicit instantiation

这里的模板与函数调用 Add(x, m) 不匹配,因为该模板要求两个函数参数的类型相同。但通过使用 Add<double>(x, m),可强制为 double 类型实例化,并将参数 m 强制转换为 double 类型,以便与函数 Add<double>(double, double) 的第二个参数匹配。

int m = 5;
double x = 14.3;
swap_func<double>(m, x);  // almost works

This generates an explicit instantiation for type double. Unfortunately, in this case, the code won’t work because the first formal parameter, being type double &, can’t refer to the type int variable m.
这将为类型 double 生成一个显式实例化。不幸的是,这些代码不管用,因为第一个形参的类型为double &,不能指向 int 变量 m

Implicit instantiations, explicit instantiations, and explicit specializations collectively are termed specializations.What they all have in common is that they represent a function definition that uses specific types rather than one that is a generic description.
隐式实例化、显式实例化和显式具体化统称为具体化 (specialization)。它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述。

The addition of the explicit instantiation led to the new syntax of using template and template <> prefixes in declarations to distinguish between the explicit instantiation and the explicit specialization.
引入显式实例化后,必须使用新的语法,在声明中使用前缀 templatetemplate <>,以区分显式实例化和显式具体化。

...
template <class T>
void swap_func(T &, T &);  // template prototype

template <>
void swap_func<job>(job &, job &);  // explicit specialization for job

int main(void) {
	template
	void swap_func<char>(char &, char &);  // explicit instantiation for char

	short a, b;
	...
	swap_func(a, b);  // implicit template instantiation for short
	job n, m;
	...
	swap_func(n, m);  // use explicit specialization for job
	char g, h;
	...
	swap_func(g, h);  // use explicit template instantiation for char
	...
}

编译器看到 char 的显式实例化后,将使用模板定义来生成 swap_func()char 版本。对于其它 swap_func() 调用,编译器根据函数调用中实际使用的参数,生成相应的版本。例如,当编译器看到函数调用 swap_func(a, b); 后,将生成 swap_func()short 版本,因为两个参数的类型都是 short。当编译器看到 swap_func(n, m); 后,将使用为 job 类型提供的独立定义 (显式具体化) 。当编译器看到 swap_func(g, h); 后,将使用处理显式实例化时生成的模板具体化。

6. Which Function Version Does the Compiler Pick? (编译器选择使用哪个函数版本)

What with function overloading, function templates, and function template overloading, C++ needs, and has, a well-defined strategy for deciding which function definition to use for a function call, particularly when there are multiple arguments.The process is called overload resolution.
对于函数重载、函数模板和函数模板重载,C++ 需要一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析 (overloading resolution)。

Let’s take just a broad look at how the process works:

  • Phase 1 - Assemble a list of candidate functions.These are functions and template functions that have the same names as the called functions.
    创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。

  • Phase 2 - From the candidate functions, assemble a list of viable functions.These are functions with the correct number of arguments and for which there is an implicit conversion sequence, which includes the case of an exact match for each type of actual argument to the type of the corresponding formal argument. For example, a function call with a type float argument could have that value converted to a double to match a type double formal parameter, and a template could generate an instantiation for float.
    使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用 float 参数的函数调用可以将该参数转换为 double,从而与 double 形参匹配,而模板可以为 float 生成一个实例。

  • Phase 3 - Determine whether there is a best viable function. If so, you use that function. Otherwise, the function call is an error.
    确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。

Consider a case with just one function argument - for example, the following call:

may('B');  // actual argument is type char

编译器将寻找候选者,即名称为 may() 的函数和函数模板。然后寻找那些可以用一个参数调用的函数。例如,下面的函数符合要求,因为其名称与被调用的函数相同,且可只给它们传递一个参数:

void may(int);  // #1
float may(float, float = 3);  // #2
void may(char);  // #3
char * may(const char *);  // #4
char may(const char &);  // #5
template<class T> void may(const T &);  // #6
template<class T> void may(T *);  // #7

注意,只考虑特征标,而不考虑返回类型。其中的两个候选函数 (#4 和 #7) 不可行,因为整数类型不能被隐式地转换 (即没有显式强制类型转换) 为指针类型。剩余的一个模板可用来生成具体化,其中 T 被替换为 char 类型。这样剩下 5 个可行的函数,其中的每一个函数,如果它是声明的唯一一个函数,都可以被使用。

编译器必须确定哪个可行函数是最佳的,它查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换。通常,从最佳到最差的顺序如下所述。

  1. Exact match, with regular functions outranking templates
    完全匹配,但常规函数优先于模板。

  2. Conversion by promotion (for example, the automatic conversions of char and short to int and of float to double)
    提升转换 (例如,charshort 自动转换为 intfloat 自动转换为 double)。

  3. Conversion by standard conversion (for example, converting int to char or long to double)
    标准转换 (例如,int 转换为 charlong 转换为 double)。

  4. User-defined conversions, such as those defined in class declarations
    用户定义的转换,如类声明中定义的转换。

例如,函数 #1 优于函数 #2,因为 charint 的转换是提升转换,而 charfloat 的转换是标准转换。函数 #3、函数 #5 和函数 #6 都优于函数 #1 和 #2,因为它们都是完全匹配的。#3 和 #5 优于 #6,因为 #6 函数是模板。如果两个函数 (如 #3 和 #5) 都完全匹配,将如何办呢?通常,有两个函数完全匹配是一种错误,但这一规则有两个例外。

(1) Exact Matches and Best Matches (完全匹配和最佳匹配)

进行完全匹配时,C++ 允许某些无关紧要的转换。下表列出了这些转换,Type 表示任意类型。例如,int 实参与 int & 形参完全匹配。注意,Type 可以是 char & 这样的类型,因此这些规则包括从 char &const char & 的转换。Type (argument-list) 意味着用作实参的函数名与用作形参的函数指针只要返回类型和参数列表相同,就是匹配的。

Trivial Conversions Allowed for an Exact Match (完全匹配允许的无关紧要转换)
在这里插入图片描述

Suppose you have the following function code:

struct blot { int a; char b[10]; };
blot ink = { 25, "spots" };
...
recycle(ink);

In that case, all the following prototypes would be exact matches:

void recycle(blot);  // #1 blot-to-blot
void recycle(const blot);  // #2 blot-to-(const blot)
void recycle(blot &);  // #3 blot-to-(blot &)
void recycle(const blot &);  // #4 blot-to-(const blot &)

如果有多个匹配的原型,则编译器将无法完成重载解析过程。如果没有最佳的可行函数,则编译器将生成一条错误消息,该消息可能会使用诸如 ambiguous (二义性) 这样的词语。

First, pointers and references to non-const data are preferentially matched to non-const pointer and reference parameters.
有时候,即使两个函数都完全匹配,仍可完成重载解析。首先,指向非 const 数据的指针和引用优先与非 const 指针和引用参数匹配。在 recycle() 示例中,如果只定义了函数 #3 和 #4 是完全匹配的,则将选择 #3,因为 ink 没有被声明为 const。然而,const 和 非 const 之间的区别只适用于指针和引用指向的数据。 如果只定义了 #1 和 #2,则将出现二义性错误。

一个完全匹配优于另一个的另一种情况是,其中一个是非模板函数,而另一个不是。在这种情况下, 非模板函数将优先于模板函数 (包括显式具体化)。

如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。例如,这意味着显式具体化将优于使用模板隐式生成的具体化:

struct blot { int a; char b[10]; };
template <class Type> void recycle(Type t);  // template
template <> void recycle<blot>(blot & t);  // specialization for blot
...
blot ink = { 25, "spots" };
...
recycle(ink);  // use specialization

The term most specialized doesn’t necessarily imply an explicit specialization; more generally, it indicates that fewer conversions take place when the compiler deduces what type to use.
最具体 (most specialized) 并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少。

template <class Type> void recycle(Type t);  // #1
template <class Type> void recycle(Type * t);  // #2

Suppose the program that contains those templates also contains the following code:

struct blot { int a; char b[10]; };
blot ink = { 25, "spots" };
...
recycle(&ink);  // address of a structure

The recycle(&ink) call matches Template #1, with Type interpreted as blot *.The recycle(&ink) function call also matches Template #2, this time with Type being ink. This combination sends two implicit instantiations, recycle<blot *>(blot *) and recycle<blot>(blot *), to the viable function pool.
recycle(&ink) 调用与 #1 模板匹配,匹配时将 Type 解释为 blot *
recycle(&ink) 调用与 #2 模板匹配,匹配时将 Type 解释为 blot
因此将两个隐式实例,recycle<blot *>(blot *)recycle <blot>(blot *) 发送到可行函数池中。

Of these two template functions, recycle<blot *>(blot *) is considered the more specialized because it underwent fewer conversions in being generated.That is, Template #2 already explicitly said that the function argument was pointer-to-Type, so Type could be directly identified with blot. However, Template #1 had Type as the function argument, so Type had to be interpreted as pointer-to blot.That is, in Template #2, Type was already specialized as a pointer, hence it is “more specialized.
在这两个模板函数中,recycle<blot>(blot *) 被认为是更具体的。因为在生成过程中,它需要进行的转换更少。也就是说,Template #2 已经显式指出,函数参数是指向 Type 的指针,因此可以直接用 blot 标识 Type;而 Template #1 将 Type 作为函数参数,因此 Type 必须被解释为指向 blot 的指针。也就是说,在 Template #2 中,Type 已经被具体化为指针, 因此说它更具体。

The rules for finding the most specialized template are called the partial ordering rules for function templates.
用于找出最具体的模板的规则被称为函数模板的部分排序规则 (partial ordering rules)。

(2) A Partial Ordering Rules Example (部分排序规则示例)

有两个用来显示数组内容的模板定义。第一个定义 (模板 A) 假设作为参数传递的数组中包含了要显示的数据,第二个定义 (模板 B) 假设数组元素为指针,指向要显示的数据。

// template overloading

#include <iostream>

template <typename T>  // template A
void ShowArray(T arr[], int n);

template <typename T>  // template B
void ShowArray(T *arr[], int n);

struct debts {
	char name[50];
	double amount;
};

int main() {
	using namespace std;

	int things[6] = { 13, 31, 103, 301, 310, 130 };
	struct debts mr_e[3] =
	{
		{ "Ima Wolfe", 2400.0 },
		{ "Ura Foxe", 1300.0 },
		{ "Iby Stout", 1800.0 }
	};
	double *pd[3];

	// set pointers to the amount members of the structures in mr_e
	for (int i = 0; i < 3; ++i) {
		pd[i] = &mr_e[i].amount;
	}

	cout << "Listing Mr. E's counts of things:\n";
	// things is an array of int
	ShowArray(things, 6);  // uses template A
	cout << "Listing Mr. E's debts:\n";
	// pd is an array of pointers to double
	ShowArray(pd, 3);  // uses template B (more specialized)
	// cin.get();
	return 0;
}

template <typename T>
void ShowArray(T arr[], int n) {
	using namespace std;

	cout << "template A\n";
	for (int i = 0; i < n; ++i) {
		cout << arr[i] << ' ';
	}
	cout << endl;
}

template <typename T>
void ShowArray(T *arr[], int n) {
	using namespace std;

	cout << "template B\n";
	for (int i = 0; i < n; ++i) {
		cout << *arr[i] << ' ';
	}
	cout << endl;
}

Listing Mr. E's counts of things:
template A
13 31 103 301 310 130
Listing Mr. E's debts:
template B
2400 1300 1800
请按任意键继续. . .

Consider this function call:

ShowArray(things, 6);

The identifier things is the name of an array of int, so it matches the following template with T taken to be type int:

template <typename T>  // template A
void ShowArray(T arr[], int n);

Next, consider this function call:

ShowArray(pd, 3);

Here, pd is the name of an array of double *.This could be matched by Template A:

template <typename T>  // template A
void ShowArray(T arr[], int n);

Here, T would be taken to be type double *. In this case, the template function would display the contents of the pd array: three addresses.The function call could also be matched by Template B:

template <typename T>  // template B
void ShowArray(T *arr[], int n);

In this case, T is type double, and the function displays the dereferenced elements *arr[i] - that is, the double values pointed to by the array contents. Of the two templates,Template B is the more specialized because it makes the specific assumption that the array contents are pointers, so it is the template that gets used.

如果将模板 B 从程序中删除,则编译器将使用模板 A 来显示 pd 的内容,因此显示的将是地址,而不是值。

In short, the overload resolution process looks for a function that’s the best match. If there’s just one, that function is chosen. If more than one are otherwise tied, but only one is a non template function, that non template function is chosen. If more than one candidate are otherwise tied and all are template functions, but one template is more specialized than the rest, that one is chosen. If there are two or more equally good non template functions, or if there are two or more equally good template functions, none of which is more specialized than the rest, the function call is ambiguous and an error. If there are no matching calls, of course, that is also an error.
简而言之,重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数;如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体,则选择该函数。如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体, 则函数调用将是不确定的,因此是错误的;当然,如果不存在匹配的函数,则也是错误。

(3) Making Your Own Choices (自己选择)

在有些情况下,可通过编写合适的函数调用,引导编译器做出您希望的选择。下面程序将模板函数定义放在文件开头,从而无需提供模板原型。与常规函数一样,通过在使用函数前提供模板函数定义,它让它也充当原型。

// choosing a template

#include <iostream>

// #1
template <class T>
T lesser(T a, T b) {
	return a < b ? a : b;
}

// #2
int lesser(int a, int b) {
	a = (a < 0) ? -a : a;
	b = (b < 0) ? -b : b;
	return (a < b) ? a : b;
}

int main() {
	using namespace std;

	int m = 20;
	int n = -30;
	double x = 15.5;
	double y = 25.9;

	cout << lesser(m, n) << endl;  // use #2
	cout << lesser(x, y) << endl;  // use #1 with double
	cout << lesser<>(m, n) << endl;  // use #1 with int
	cout << lesser<int>(x, y) << endl; // use #1 with int

	// cin.get();
	return 0;
}

最后的函数调用将 double 转换为 int,有些编译器会针对这一点发出警告。

20
15.5
-30
15
请按任意键继续. . .

上述程序提供了一个模板和一个标准函数,其中模板返回两个值中较小的一个,而标准函数返回两个值中绝对值较小的那个。如果函数定义是在使用函数前提供的,它将充当函数原型,因此这个示例无需提供原型。

Consider the following statement:

cout << lesser(m, n) << endl;  // use #2

The function call arguments match both the template function and the non template function, so the non template function is chosen, and it returns the value 20.

Next, the function call in the statement matches the template, with type T taken to be double:

cout << lesser(x, y) << endl;  // use #1 with double

Now consider this statement:

cout << lesser<>(m, n) << endl;  // use #1 with int

The presence of the angle brackets in lesser<>(m, n) indicates that the compiler should choose a template function rather than a non template function, and the compiler, noting that the actual arguments are type int, instantiates the template using int for T.
lesser<>(m, n) 中的 <> 指出,编译器应选择模板函数,而不是非模板函数。编译器注意到实参的类型为 int,因此使用 int 替代 T 对模板进行实例化。

Finally, consider this statement:

cout << lesser<int>(x, y) << endl; // use #1 with int

Here we have a request for an explicit instantiation using int for T, and that’s the function that gets used.The values of x and y are type cast to type int, and the function returns an int value, which is why the program displays 15 instead of 15.5.
这条语句要求进行显式实例化 (使用 int 替代 T),将使用显式实例化得到的函数。xy 的值将被强制转换为 int,该函数返回一个 int 值,这就是程序显示 15 而不是 15.5 的原因所在。

(4) Functions with Multiple Type Arguments (多个参数的函数)

将有多个参数的函数调用与有多个参数的原型进行匹配时,情况将非常复杂。编译器必须考虑所有参数的匹配情况。如果找到比其他可行
函数都合适的函数,则选择该函数。一个函数要比其他函数都合适,其所有参数的匹配程度都必须不比其他函数差,同时至少有一个参数的匹配程度比其他函数都高。

7. Summary

Function templates automate the process of overloading functions.You define a function by using a generic type and a particular algorithm, and the compiler generates appropriate function definitions for the particular argument types you use in a program.
函数模板自动完成重载函数的过程。只需使用泛型和具体算法来定义函数,编译器将为程序中使用的特定参数类型生成正确的函数定义。

References

[1] Yongqiang Cheng, https://yongqiang.blog.csdn.net/
[2] C++ Primer, Fifth Edition, https://www.oreilly.com/library/view/c-primer-fifth/9780133053043/
[3] C++ Primer Plus, Sixth Edition, https://www.oreilly.com/library/view/c-primer-plus/9780132781145/

  • 46
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yongqiang Cheng

梦想不是浮躁,而是沉淀和积累。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值