第 1 章 函数模板

本章将介绍函数模板。函数模板是被参数化的函数,因此他们代表的是一组具有相似行为的函数。

1.1 函数模板初探

函数模板提供了适用于不同数据类型的函数行为。也就是说,函数模板代表的是一组函数。除了某些信息未被明确指定之外,他们看起来很像普通函数。这些未被指定的信息就是被参数化的信息。我们将通过下面一个简单的例子来说明这一问题。

1.1.1 定义模板

以下就是一个函数模板, 它返回两个数之中的最大值:

1
2
3
4
5
6
template<typename T>
T max(T a, T b)
{
// 如果 b < a, 返回 a,否则返回 b
return b < a ? a : b;
}

这个模板定义了一组函数,它们都返回函数的两个参数中值较大的那一个。这两个参数的类型并没有被明确指定,而是被表示为模板参数 T。如你所见,模板参数必须按照如下语法声明:

template<由逗号分割的模板参数>

在我们的例子中,模板参数是 typename T。请留意 <> 的使用,它们在这里被称为尖括号。关键字 typename 标识了一个类型参数。这是到目前为止 C++ 中模板参数最典型的用法,当然也有其他参数(非类型模板参数),我们将在第 3 章介绍。

在这里 T 是类型参数。你可以用任意标识作为类型参数名,但是习惯上是用 T。类型参数可以代表任意类型,它在模板被调用的时候决定。但是该类型(可以是基础类型,类或者其它类型)应该支持模板中用到的运算符。在本例中,类型 T 必须支持小于运算符,因为 ab 在做比较时用到了它。例子中不太容易看出的一点是,为了支持返回值, T 还应该是可拷贝的。

由于历史原因,除了 typename 之外你还可以使用 class 来定义类型参数。关键字 typename 在 C++98 标准发展过程中引入的较晚。在那之前,关键字 class 是唯一可以用来定义类型参数的方法,而且目前这一方法依然有效。因此模板 max() 也可以被定义成如下等效的方式:

1
2
3
4
5
6
7
// basics/max1.hpp
template<class T>
T max(T a, T b)
{
// 如果 b < a, 返回 a,否则返回 b
return b < a ? a : b;
}

从语义上来讲,这样写不会有任何不同。因此,在这里你依然可以使用任意类型作为类型参数。只是用 class 的话可能会引起一些歧义(T 并不是只能是 class 类型),你应该优先使用 typename。但是与定义 class 的情况不同,在声明模板类型参数的时候,不可以用关键字 struct 取代 typename

1.1.2 使用模板

下面的程序展示了使用模板的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// basics/max1.cpp
#include "max1.hpp"
#include <iostream>
#include <string>

int main()
{
int i = 42;
std::cout << "max(7,i): " << ::max(7,i) << '\n';
double f1 = 3.4;
double f2 = -6.7;
std::cout << "max(f1,f2): " << ::max(f1,f2) << '\n';
std::string s1 = "mathematics";
std::string s2 = "math";
std::cout << "max(s1,s2): " << ::max(s1,s2) << '\n';
}

在这段代码中,max()被调用了三次:一次是比较两个int,一次是比较两个double,还有一次是比较两个std::string。每一次都会算出最大值。下面是输出结果:

max(7,i): 42
max(f1,f2): 3.4
max(s1,s2): mathematics

注意在调用max()模板的时候使用了作用域限制符::。这样程序将会在全局作用域中查找max()模板。否则的话,在某些情况下标准库中的std::max()模板将会被调用,或者有时候不太容易确定具体哪一个模板会被调用。

在编译阶段,模板并不是被编译成一个可以支持多种类型的实体。而是对每一个用于该模板的类型都会产生一个独立的实体。因此在本例中,max()会被编译出三个实体,因为它被用于三种类型。比如第一次调用时:

int i = 42;
... max(7,i) ...

函数模板的类型参数是int。 因此语义上等效于调用了如下函数:

1
2
3
4
int max (int a, int b)
{
return b < a ? a : b;
}

以上用具体类型取代模板类型参数的过程叫做“实例化”。它会产生模板的一个实例。

值得注意的是,模板的实例化不需要程序员做额外的请求,只是简单的使用函数模板就会触发这一实例化过程。

同样的,另外两次调用也会分别为doublestd::string各实例化出一个实例,就像是分别定义了下面两个函数一样:

double max (double, double);
std::string max (std::string, std::string);

另外,只要结果是有意义的,void作为模板参数也是有效的。比如:

1
2
3
4
5
6
template<typename T>
T foo(T*)
{ }

void* vp = nullptr;
foo(vp); // OK: 推导出 void foo(void*)

1.1.3 两阶段编译检查

在实例化模板的时候,如果模板参数类型不支持所有模板中用到的操作符,将会遇到编译期错误。比如:

1
2
3
std::complex<float> c1, c2; // std::complex<> 没有提供小于运算符
...
::max(c1, c2); // 编译期 ERROR

但是在定义的地方并没有遇到错误提示。这是因为模板是被分两步编译的:

  1. 在模板定义阶段, 模板的检查并不包含类型参数的检查。 只包含下面几个方面:
    - 语法检查。比如少了分号。
    - 使用了未定义的不依赖于模板参数的名称(类型名,函数名,……)。
    - 未使用模板参数的静态断言。
  2. 在模板实例化阶段, 为确保所有代码都是有效的, 模板会再次被检查, 尤其是那些依赖于类型参数的部分。

比如:

1
2
3
4
5
6
7
8
template<typename T>
void foo(T t)
{
undeclared(); // 如果 undeclared() 未定义,第一阶段就会报错,因为与模板参数无关
undeclared(t); // 如果 undeclared(t) 未定义,第二阶段会报错,因为与模板参数有关
static_assert(sizeof(int) > 10, "int too small"); // 如果 sizeof(int) <= 10,总是会报错
static_assert(sizeof(T) > 10, "T too small"); // 如果实例化出的 T的 size <= 10,则会失败
}

名称被检查两次这一现象被称为“两阶段查找”,在 14.3.1 节中会进行更细致的讨论。

需要注意的是,有些编译器并不会执行第一阶段中的所有检查。因此如果模板没有被至少实例化一次的话,你可能一直都不会发现代码中的常规错误。

编译和链接

两阶段的编译检查给模板的处理带来了一个问题:当实例化一个模板的时候,编译器需要(一定程度上)看到模板的完整定义。这不同于函数编译和链接分离的思想,函数在编译阶段只需要声明就够了。第 9 章将讨论如何应对这一问题。我们将暂时采取最简单的方法:将模板的实现写在头文件里。

1.2 模板实参推导

当我们调用形如max()的函数模板来处理某些变量时,模板参数将由被传递的调用参数决定。如果我们传递两个int类型的参数给模板函数,C++ 编译器会将模板参数T推断为int

不过T可能只是实际传递的函数参数类型的一部分。比如我们定义了如下接受常量引用作为函数参数的模板:

1
2
3
4
5
template<typename T>
T max (T const& a, T const& b)
{
return b < a ? a : b;
}

此时如果我们传递int类型的调用参数,由于调用参数和int const&匹配,类型参数T将被推断为int


http://example.com/2023/08/15/cpp-template-ch/cpp-template-ch01/
作者
QiDianMaker
发布于
2023年8月14日
许可协议