C++ Templates (1.1 初窥函数模板 A First Look at Function Templates)

返回完整目录

1.1 初窥函数模板 A First Look at Function Templates

函数模板提供了一种针对不同类型的可以被调用的函数行为,换句话说,一个函数模板代表一系列的函数。函数模板和普通函数很像,除了函数的一部分元素有待确定:这些元素是参数化的(parameterized)。为了方便描述,首先看一个简单的例子。

1.1.1 定义模板 Defining the Template

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

// basics/max1.hpp
template <typename T>
T max(T a, T b)
{
      // if b<a then yield "a" else yield "b"
      return b < a ? a : b;
}

该模板定义指定了一系列返回两个值中最大值的函数,这两个值作为函数参数a和b进行传递[1]。这些参数的类型由模板参数(template parameter)T确定。如该例子中所见,模板参数必须通过如下的语法形式进行声明:

template <comma-separated-list-of-parameters(用逗号隔开的参数列表)>

本例中,(模板)参数列表为typename T。符号<>为一对尖括号(angle brackets),请注意该符号的使用。关键字typename引入了一个类型参数(type parameter)。虽然这是到目前为止最常用的一种C++程序模板参数,但其他的参数也是有可能的,将在后续章节中讨论(详见第3章)。

该例中,类型参数为T,可以使用任何标识符(identifier)作为参数名字,但使用T是典型的方式。类型参数代表任何一种参数,并且由调用者调用该函数时确定。可以使用任何类型(基础类型(fundamental type)类(class)等等),只要该类型提供了模板中使用的操作(operation)。本例中,类型T必须支持<操作,因为a和b通过该操作符(operator)进行比较。可能不太明显的是,根据max()函数的定义,类型T的值必须是能够拷贝的(copyable),以使得该值能够被返[2]

由于历史原因,也可以使用关键字class代替typename来定义一个类型参数。关键字typename出现相对晚一些,在C++98标准进化的过程中出现。在那之前,关键字class是引入类型参数的唯一方法,并且该方法依然保持有效。因此,模板函数max()可以等效地被定义成如下形式:

template <class T>
T max(T a, T b)
{
      return b < a ? a : b;
}

该情形下,这两种定义没有任何区别。因此,尽管使用关键字class,任何类型可以作为模板参数。然而,此处使用关键字class会有误导性(不是只有class的类型才能够用于替换T,基本类型也可以),因此更建议使用关键字typename。不幸的是,不像类类型声明(class type declaration),当声明类型参数时关键字struct不能用于替换typename

1.1.2 使用模板 Using the Template

以下程序展示了如何使用max()函数模板:

// 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()模板,在一定条件下可能被调用或者引起不明确(ambiguity)[3]

模板不会被编译成一个可以处理任何类型的单个实体。相反,针对模板使用的每一个类型,对应每一个类型的不同实体从模板中生成[4]。因此,max()为三个类型分别进行编译。比如,第一个max()调用

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

使用int作为模板参数T的函数模板。因此,它具有调用如下代码的语义:

int max(int a, int b)
{
      return b < a ? a : b;
}

这个使用实体类型代替模板参数的过程称为实例化(instantiation),并生成模板的实例(instance)[5]

仅仅使用函数模板便可触发一个实例化的过程,并不需要额外单独请求实例化。

相似地,其他对max()的调用也将实例化double和std::string版的max模板,就像它们被分别以如下方式声明和实现:

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

void也可以作为有效的模板实参,只要生成的代码是有效的即可。比如:

template <typename T>
T foo(T*)
{
}

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

1.1.3 二阶段翻译(二次翻译) Two-Phase Translation

如果用某个类实例化模板,但该类不支持模板中所有操作将导致编译期错误(compile-time error),比如:

std::complex<float> c1, c2; //未提供<操作
...
::max(c1,c2); //编译期错误

模板的“编译”有两个阶段:

  1. 定义时(definition time)不进行实例化,模板代码的正确性将被检查(但不考虑模板参数),这包括

    • 发现语法错误,比如丢失分号等;

    • 发现使用不依赖于模板参数的未知名字(类型名、函数名...);

    • 检查不依赖于模板参数的静态断言(static assertion);

  2. 实例化期间(instantiation time),模板代码再一次被检查,以确保所有的代码是有效的。也就是说,所有依赖于模板参数的部分被检查了两遍(double-checked)。
    举个例子:

template <typename T>
void foo(T t)
{
      undeclared();       //如果undeclared()是未知的,将产生第一阶段编译器错误(first-phase compile-time error)
      undeclared(t);       //如果undeclared(T)是未知的,将产生第二阶段编译错误(second-phase compile-time error)
      static_assert(sizeof(int) > 10, "int too small");       //如果sizeof(int)<=10,将一直失败
      static_assert(sizeof(int) > 10, "T too small");      //如果使用sizeof(T)<=10的类型T进行实例化,将失败
}

名字被检查两遍的过程被称为二阶段检查/二次检查(two-phase lookup),该过程将在第14.3.1节中详细讨论。

一些编译器在第一阶段不执行全面检查(full check),因此在模板代码至少进行一次实例化前无法发现一般的问题[6]

编译和链接
在实践中处理模板过程中,二阶段翻译将导致一个重要的问题:当一个函数模板被使用并触发实例化时,编译器(有时候)需要看模板的定义。这破坏了普通函数(ordinary function)的常规编译和链接之间的差别,即在编译期只需要函数的声明(the declaration of a function)便可编译使用函数的代码。处理该问题的方法在第9章中进行讨论。此时,可以使用最简单的方法:在头文件中实现每一个模板

脚注


  1. 注意到max()模板依据[StepanovNotes]有意地返回“b < a ? a : b”而不是“a < b ? b : a”来确保函数正确,尽管这两个值相等但并不等价(equivalent but not equal)。进一步讨论可参考知乎:b < a ? a : b 和 a < b ? b : a 有什么不同?博客园:为什么 max() 应该写成 b < a ? a : b 呢? ↩︎

  2. C++17以前,类型T必须是可拷贝的,才能进行参数传递给函数。但是自从C++17,可以传递临时量(temporaries)(右值,见附录B)尽管没有有效的拷贝构造和移动构造。 ↩︎

  3. 比如说,如果一种参数类型在std命名空间中被定义(如std::string),根据C++查找规则,全局的和std命名空间中的max()都可以被发现(参见附录C)。 ↩︎

  4. “一个实体适用于所有类型”的方案是可信的,但是实践中没有被采用(这将导致运行时效率降低)。所有的语言规则都是基于一个原则:不同的模板参数生成不同的实体。 ↩︎

  5. 在面向对象编程中,术语实例和实例化也被使用,指一个类的实例(concrete object of a class)。然而由于此书关于模板,该术语指使用模板的语境,除非特殊说明。 ↩︎

  6. 比如说,一些版本的Visual C++的编译器(如Visual Studio 2013 和 2015)允许不依赖于模板参数的未声明的名字,甚至允许一些语法缺陷(syntax flaws),比如丢失分号。 ↩︎

posted @ 2020-08-05 22:33  失落孤舟  阅读(238)  评论(0编辑  收藏  举报