第 1 章 函数模板
本章将介绍函数模板。函数模板是被参数化的函数,因此他们代表的是一组具有相似行为的函数。
1.1 函数模板初探
函数模板提供了适用于不同数据类型的函数行为。也就是说,函数模板代表的是一组函数。除了某些信息未被明确指定之外,他们看起来很像普通函数。这些未被指定的信息就是被参数化的信息。我们将通过下面一个简单的例子来说明这一问题。
1.1.1 定义模板
以下就是一个函数模板, 它返回两个数之中的最大值:
1 |
|
这个模板定义了一组函数,它们都返回函数的两个参数中值较大的那一个。这两个参数的类型并没有被明确指定,而是被表示为模板参数 T
。如你所见,模板参数必须按照如下语法声明:
template<由逗号分割的模板参数>
在我们的例子中,模板参数是 typename T
。请留意 <
和 >
的使用,它们在这里被称为尖括号。关键字 typename
标识了一个类型参数。这是到目前为止 C++ 中模板参数最典型的用法,当然也有其他参数(非类型模板参数),我们将在第 3 章介绍。
在这里 T
是类型参数。你可以用任意标识作为类型参数名,但是习惯上是用 T
。类型参数可以代表任意类型,它在模板被调用的时候决定。但是该类型(可以是基础类型,类或者其它类型)应该支持模板中用到的运算符。在本例中,类型 T
必须支持小于运算符,因为 a
和 b
在做比较时用到了它。例子中不太容易看出的一点是,为了支持返回值, T
还应该是可拷贝的。
由于历史原因,除了 typename
之外你还可以使用 class
来定义类型参数。关键字 typename
在 C++98 标准发展过程中引入的较晚。在那之前,关键字 class
是唯一可以用来定义类型参数的方法,而且目前这一方法依然有效。因此模板 max()
也可以被定义成如下等效的方式:
1 |
|
从语义上来讲,这样写不会有任何不同。因此,在这里你依然可以使用任意类型作为类型参数。只是用 class
的话可能会引起一些歧义(T 并不是只能是 class
类型),你应该优先使用 typename
。但是与定义 class
的情况不同,在声明模板类型参数的时候,不可以用关键字 struct
取代 typename
。
1.1.2 使用模板
下面的程序展示了使用模板的方法:
1 |
|
在这段代码中,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 |
|
以上用具体类型取代模板类型参数的过程叫做“实例化”。它会产生模板的一个实例。
值得注意的是,模板的实例化不需要程序员做额外的请求,只是简单的使用函数模板就会触发这一实例化过程。
同样的,另外两次调用也会分别为double
和std::string
各实例化出一个实例,就像是分别定义了下面两个函数一样:
double max (double, double);
std::string max (std::string, std::string);
另外,只要结果是有意义的,void
作为模板参数也是有效的。比如:
1 |
|
1.1.3 两阶段编译检查
在实例化模板的时候,如果模板参数类型不支持所有模板中用到的操作符,将会遇到编译期错误。比如:
1 |
|
但是在定义的地方并没有遇到错误提示。这是因为模板是被分两步编译的:
- 在模板定义阶段, 模板的检查并不包含类型参数的检查。 只包含下面几个方面:
- 语法检查。比如少了分号。
- 使用了未定义的不依赖于模板参数的名称(类型名,函数名,……)。
- 未使用模板参数的静态断言。 - 在模板实例化阶段, 为确保所有代码都是有效的, 模板会再次被检查, 尤其是那些依赖于类型参数的部分。
比如:
1 |
|
名称被检查两次这一现象被称为“两阶段查找”,在 14.3.1 节中会进行更细致的讨论。
需要注意的是,有些编译器并不会执行第一阶段中的所有检查。因此如果模板没有被至少实例化一次的话,你可能一直都不会发现代码中的常规错误。
编译和链接
两阶段的编译检查给模板的处理带来了一个问题:当实例化一个模板的时候,编译器需要(一定程度上)看到模板的完整定义。这不同于函数编译和链接分离的思想,函数在编译阶段只需要声明就够了。第 9 章将讨论如何应对这一问题。我们将暂时采取最简单的方法:将模板的实现写在头文件里。
1.2 模板实参推导
当我们调用形如max()
的函数模板来处理某些变量时,模板参数将由被传递的调用参数决定。如果我们传递两个int
类型的参数给模板函数,C++ 编译器会将模板参数T
推断为int
。
不过T
可能只是实际传递的函数参数类型的一部分。比如我们定义了如下接受常量引用作为函数参数的模板:
1 |
|
此时如果我们传递int
类型的调用参数,由于调用参数和int const&
匹配,类型参数T
将被推断为int
。