第16章 模板与泛型编程

模板是C++中泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。我们先从模板的定义开始:

16.1 定义模板

(1)模板的定义以关键字template开始,后面跟一个模板参数列表,用<>括起来。
(2)参数列表中,类型参数前必须使用关键字class或typename。
(3)模板有类型参数(type parameter)非类型参数(nontype parameter)之分。

  1. 函数模板
    我们定义一个通用的函数模板(function template),而不是为每个类型都定义一个新函数:

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

    (1)类型参数
    如上所述的compare函数有一个模板类型参数T。一般来说,
    (a)我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用;
    (b)类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。

    (2)非类型参数
    (a)一个非类型参数表示一个值而非一个类型;
    (b)我们通过一个特定的类型名而非关键字class或typename来指定非类型参数;
    (c)当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式!

    当我们调用这个版本的compare时:
    compare(“hi”, “mom”);
    编译器会使用字面常量的大小来代替N和M,从而实例化模板。如下:
    int compare(const char (&p1)[3], const char (&p2)[4] )
    NOTE:非类型模板参数的模板实参必须是常量表达式。

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

    (4)模板编译
    当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。
    与非模板代码不同,模板的头文件通常既包括声明也包括定义。
    NOTE:函数模板和类模板成员函数的定义通常放在头文件中。

  2. 类模板
    类模板(class template)是用来生成类的蓝图的。与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。(1)定义类模板:在类模板(及其成员)的定义中,我们将模板参数当做替身,代替使用模板时用户需要提供的类型或值:

    (2)实例化模板
    当使用一个类模板时,我们必须提供额外信息。我们现在知道这些额外信息是显式模板实参(explicit template argument)列表,它们被绑定到模板参数。例如:

    NOTE:一个类模板的每个实例都形成一个独立的类。Bolb<string>与任何其他Bolb类型没有关联,也不会对任何其他Bolb类型的成员有特殊访问权限。

    (3)类模板的成员函数
    我们既可以在类模板内部,也可以在外部为其定义成员函数。定义在类模板之外的成员函数必须以关键字template开始,后接类模板参数列表。

    (4)在类代码内简化模板类型的使用
    在类模板自己的作用域中,我们可以直接使用模板名而不提供实参:

    上述BolbPtr的前置递增和递减成员返回BolbPtr&,而不是BolbPtr<T>&。当我们处于一个类模板的作用域时,编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样。
    NOTE:在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参。

    (5)令模板自己的类型参数成为友元
    在C++11新标准中,我们可以将模板类型参数声明为友元:

    因此,对于某个类型名Foo,Foo将成为Bar<Foo>的友元。

    (6)模板类型别名
    (a)类模板的一个实例定义了一个类型,与任何其他类类型一样,我们可以定义一个typedef来引用实例化的类:typedef Blob<string> StrBlob;
    (b)由于模板不是一个类型,我们不能定义一个typedef引用一个模板。即,无法定义一个typedef引用Blob<T>。
    (c)但是,C++11新标准允许我们为类模板定义一个类型别名:

    (d)当我们定义一个模板类型别名是,可以固定一个或多个模板参数:

  3. 模板参数
    一个模板参数的名字也没有什么内在含义,我们通常将类型参数命名为T,但实际上我们可以使用任何名字。
    1).使用类的类型成员
    (1)假定T是一个模板类型参数,当编译器遇到类似T::mem这样的代码时,他不会知道mem是一个类型成员还是一个static数据成员,直至实例化时才会知道。
    (2)但是为了处理模板,编译器必须知道名字是否表示一个类型。例如:
    T::size_type *p;
    编译器需要知道我们是在定义一个名为p的变量还是将一个名为size_type的static数据成员与名为p的变量相乘。
    (3)默认情况,C++语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。
    (4)我们通过使用关键字typename来实现这一点:

    NOTE:当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class。

    2).默认模板实参
    在新标准中,我们可以为函数和类模板提供默认实参。而更早的C++标准值允许为类模板提供默认实参。

    这段代码中,我们为模板添加了第二个类型参数,名为F,表示可调用对象的类型;并定义一个新的函数参数f,绑定到一个可调用对象上。

  4. 成员模板
    一个类可以包含本身是模板的成员函数。这种成员被称为成员模板(member template)成员模板不能是虚函数。
    (1)普通(非模板)类的成员模板
    我们定义一个类,类似unique_ptr所使用的默认删除器类型:

    我们可以用这个类代替delete:

    (2)类模板的成员模板
    在此情况下,类和成员各有自己的独立的模板参数。例如,我们将Blob类定义一个构造函数,它接受两个迭代器,表示要拷贝的元素范围,我们希望支持不同类型序列的迭代器:

    当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表:

    (3)实例化成员模板
    为了实例化一个类模板的成员模板,我们必须同时提供类和函数模板的实参:

    当我们定义a1时,显示地指出编译器应该实例化一个int版本的Blob。构造函数自己的类型参数则通过begin(ia)和end(ia)的类型来推断,结果为int*。因此,a1的定义实例化了如下版本:
    Blob<int>::Blob(int*, int*);

  5. 控制实例化
    (1)相同的实例可能出现在多个对象文件中,在多个文件中实例化相同模板的额外开销可能非常大。在新标准中,我们可以通过显示实例化(explicit instantiation)来避免这种开销。
    形式:
    extern template declaration;        //实例化声明
    template declaration;        //实例化定义
    declaration是一个类或函数声明,其中所有模板参数已被替换为模板实参,例如:

    (2)当编译器遇到extern模板声明时,他不会在本文件中生成实例代码。
    (3)将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。
    (4)对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。

  6. 效率与灵活性
    对模板设计者所面对的设计宣征,标准库智能指针类型给出了一个很好的展示。
    (1)shared_ptr和unique_ptr之间的明显不同是它们管理所保存的指针的策略——前者给予我们共享指针所有权的能力;后置则独占指针。
    (2)这两个类的另一差异是它们允许用户重载默认删除器的方式:我们可以很容器地重载一个shared_ptr的删除器,只要在创建或reset指针时传递给他一个可调用对象即可;与之相反,删除器类型是unique_ptr对象类型的一部分,用户必须在定义unique_ptr时以显示模板实参的形式提供删除器的类型。
  • 在运行时绑定删除器(shared_ptr)
    (1)虽然我们不知道标准库类型是如何实现的,但可以推断出,shared_ptr必须能直接访问其删除器。即删除器必须保存为一个指针或封装了指针的类。
    (2)我们可以确定shared_ptr不是将删除器直接保存为一个成员,因为删除器的类型运行时才会知道。
  • 在编译时绑定删除器(unique_ptr)
    现在,让我来考察unique_ptr可能的工作方式:
    (1)在这个类中,删除器的类型是类类型的一部分。即unique_ptr有两个模板参数,一个表示它所管理的指针,另一个表示删除器的类型。
    (2)由于删除器类型是unique_ptr的一部分,因此删除器成员的类型在编译时是知道的,从而删除器可以直接保存在unique_ptr对象中。

通过在编译时绑定删除器,unique_ptr避免了间接调用删除器的运行时开销。通过在运行时绑定删除器,shared_ptr使用户重载删除器更为方便。

16.2 模板实参推断

我们已经看到,对于函数模板,编译器利用调用中的函数实参来确定其模板实参。从函数实参来确定模板实参的过程被称为模板实参推断(template argument deduction)。

  1. 类型转换与模板类型参数
    (1)如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。只有很有限的几种类型转换会自动地应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。
    (2)与往常一样,顶层const无论是在形参中还是在实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括如下两项:
    (a)const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参。
    (b)数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。
    (3)其他类型转换,如算术转换,派生类向基类转换以及用户定义的转换都不能应用于函数模板。

     NOTE:将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换以及数组或函数到指针的转换。
  2. 正常类型转换应用于普通函数实参
    (1)函数模板可以有用普通类型定义的参数,即,不涉及模板类型参数的类型。
    (2)这种函数实参不进行特殊处理;它们正常转换为对应形参的类型。例如:

    NOTE:如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
  3. 函数模板显示实参
    在某些情况下,编译器无法推断出模板实参的类型。其他一些情况下,我们希望允许用户控制模板实例化。
    (1)指定显示模板实参

    本例中,没有任何函数实参的类型可用来推断T1的类型。每次调用sum时调用者都必须为T1提供一个显示模板实参(explicit template argument)。如下:

    NOTE:显示模板实参按由左至右的顺序与对应的模板参数匹配。
  4. 尾置返回类型与类型转换
    例如,我们可能希望编写一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用。但是,我们并不知道返回结果的准确类型,但知道所需类型是处理的序列的元素类型。
    由于尾置返回出现在参数列表之后,它可以使用函数的参数:
  5. 函数指针和实参推断
    (1)当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
    (2)我们定义一个函数指针,它指向的函数返回int,我们可以使用该指针指向compare的一个实例:

    pf1中参数的类型决定了T的模板实参的类型。本例中,T的模板实参类型为int。
  6. 模板实参推断和引用
    为了理解如何从该函数调用进行类型推断,考虑下面的例子:

    其中函数参数p是一个模板类型参数T的引用,非常重要的是记住两点:编译器会应用正常的引用绑定规则;const是底层的,不是顶层的。
    (1)从左值引用函数参数推断类型
    1)当一个函数参数是模板类型的一个普通(左值)引用时(即,形如T&),绑定规则告诉我们,只能传递给它一个左值。
    2)实参可以是const类型,也可以不是。如果实参是const的,则T将被推断为const类型:

    (2)从右值引用函数参数推断类型
    当一个函数参数是一个右值引用(即,形如T&&)时,正常绑定规则告诉我们可以传递给它一个右值。当我们这样做时,类型推断过程类似普通左值引用函数参数的推断过程。推断出的T的类型是该右值实参的类型:

    (3)引用折叠和右值引用参数
  7. 理解std::move
  8. 转发
16.3 重载与模板

函数模板可以被另一个模板或一个普通非模板函数重载。与往常一样,名字相同的函数必须具有不同数量或类型的参数。

16.4 可变参数模板

一个可变参数模板(variadic template)就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包(parameter packet)。存在两种参数包:模板参数包,函数参数包。

16.5 模板特例化

在某些情况下,通用模板的定义对特定类型是不适合的。当我们不能使用模板版本时,可以定义类或函数模板的一个特例化版本。

但是,只有当我们传递给compare一个字符串字面常量或者一个数组时,编译器才会调用接受两个非类型模板参数的版本:

我们无法将一个指针转换为一个数组的引用,因此当参数是p1和p2时,第二个版本的copare是不可行的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注