模板和泛型编程--模板定义--第十六章 --c++ primer

第十六章 模板和泛型编程
所谓泛型编程就是以独立于任何特定类型的方式编写代码。使用泛型程序
时,我们需要提供具体程序实例所操作的类型或值。第二部分中描述的标准库的
容器、迭代器和算法都是泛型编程的例子。每种容器(如 vector)都有单一的定
义,但可以定义许多不同种类的 vector,它们的区别在于所包含的元素类型。
模板是泛型编程的基础。使用模板时可以无须了解模板的定义。本章将介绍
怎样定义自己的模板类和模板函数。
泛型编程与面向对象编程一样,都依赖于某种形式的多态性。面向对象编程
中的多态性在运行时应用于存在继承关系的类。我们能够编写使用这些类的代
码,忽略基类与派生类之间类型上的差异。只要使用基类的引用或指针,基类类
型或派生类类型的对象就可以使用相同的代码。
在泛型编程中,我们所编写的类和函数能够多态地用于跨越编译时不相关的
类型。一个类或一个函数可以用来操纵多种类型的对象。标准库中的容器、迭代
器和算法是很好的泛型编程的例子。标准库用独立于类型的方式定义每个容器、
迭代器和算法,因此几乎可以在任意类型上使用标准库的类和函数。例如,虽然
vector 的设计者不可能了解应用程序特定的类,但我们能够定义 Sales_item
对象组成的 vector。
在 C++ 中,模板是泛型编程的基础。模板是创建类或函数的蓝图或公式。
例如,标准库定义了一个类模板,该模板定义了 vector 的含义,它可以用于产
生任意数量的特定类型的 vector 类,例如,vector 或 vector。
本书第二部分介绍了怎样使用泛型类型和泛型函数,本章将介绍怎样自定义模
板。
16.1. 模板定义
假设想要编写一个函数比较两个值并指出第一个值是小于、等于还是大于第
二个值。实践中,我们可能希望定义几个这样的函数,每一个可以比较一种给定
类型的值,第一次尝试可能是定义几个重载函数:

    // returns 0 if the values are equal, -1 if v1 is smaller, 1 if v2
is smaller
     int compare(const string &v1, const string &v2)
     {
         if (v1 < v2) return -1;
         if (v2 < v1) return 1;
         return 0;
     }
     int compare(const double &v1, const double &v2)

     {
         if (v1 < v2) return -1;
         if (v2 < v1) return 1;
         return 0;
     }
这些函数几乎相同,它们之间唯一的区别是形参的类型,每个函数的函数体

是相同的。
每个要比较的类型都需要重复函数的函数体,不仅麻烦而且容易出错。更重
要的是, 需要事先知道空间可能会比较哪些类型。 如果希望将函数用于未知类型,
这种策略就不起作用了。

16.1.1. 定义函数模板
我们可以不用为每个类型定义一个新函数,而是只定义一个函数模板
(function template)。函数模板是一个独立于类型的函数,可作为一种方式,
产生函数的特定类型版本。例如,可以编写名为 compare 的函数模板,它告诉
编译器如何为我们想要比较的类型产生特定的 compare 版本。
下面是 compare 的模板版本:

    // implement strcmp-like generic compare function
     // returns 0 if the values are equal, 1 if v1 is larger, -1 if v1
is smaller
     template <typename T>
     int compare(const T &v1, const T &v2)
     {
         if (v1 < v2) return -1;
         if (v2 < v1) return 1;
         return 0;
     }
模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括

号括住的一个或多个模板形参的列表,形参之间以逗号分隔。

模板形参表不能为空。
模板形参表
模板形参表很像函数形参表,函数形参表定义了特定类型的局部变量但并不
初始化那些变量,在运行时再提供实参来初始化形参。
同样,模板形参表示可以在类或函数的定义中使用的类型或值。例如,
compare 函数声明一个名为 T 的类型形参。在 compare 内部,可以使用名字 T
引用一个类型,T 表示哪个实际类型由编译器根据所用的函数而确定。
模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形
参。非类型形参跟在类型说明符之后声明,第 16.1.5 节将进一步介绍非类型形
参。类型形参跟在关键字 class 或 typename 之后定义,例如,class T 是名
为 T 的类型形参,在这里 class 和 typename 没有区别。

使用函数模板
使用函数模板时,编译器会推断哪个(或哪些)模板实参绑定到模板形参。
一旦编译器确定了实际的模板实参,就称它实例化了函数模板的一个实例。实质
上,编译器将确定用什么类型代替每个类型形参,以及用什么值代替每个非类型
形参。推导出实际模板实参后,编译器使用实参代替相应的模板形参产生编译该
版本的函数。编译器承担了为我们使用的每种类型而编写函数的单调工作。
对于以下调用

     int main ()
     {
         // T is int;
         // compiler instantiates int compare(const int&, const int&)
         cout << compare(1, 0) << endl;
         // T is string;
         // compiler instantiates int compare(const string&, const
string&)
         string s1 = "hi", s2 = "world";
         cout << compare(s1, s2) << endl;
         return 0;
     }
编译器将实例化 compare 的两个不同版本,编译器将用 int 代替 T 创建

第一个版本,并用 string 代替 T 创建第二个版本。

inline 函数模板
函数模板可以用与非模板函数一样的方式声明为 inline。说明符放在模板
形参表之后、返回类型之前,不能放在关键字 template 之前。

     // ok: inline specifier follows template parameter list
     template <typename T> inline T min(const T&, const T&);
     // error: incorrect placement of inline specifier
     inline template <typename T> T min(const T&, const T&);

Exercises Section 16.1.1
Exercise
16.1:
编写一个模板返回形参的绝对值。至少用三种不同类型的
值调用模板。注意:在第 16.3 节讨论编译器怎样处理模
板实例化之前,你应该将每个模板定义和该模板的所有使
用放在同一文件中。
Exercise
16.2:
编写一个函数模板,接受一个 ostream 引用和一个值,
将该值写入流。用至少四种不同类型调用函数。通过写至
cout、写至文件和写至 stringstream 来测试你的程序。
Exercise
16.3:
当调用两个 string 对象的 compare 时,传递用字符串
字面值初始化的两个 string 对象。如果编写以下代码会
发生什么?
compare (“hi”, “world”);

16.1.2. 定义类模板
就像可以定义函数模板一样,也可以定义类模板。

为了举例说明类模板,我们将为标准库 queue 类(第 9.7 节)

实现一个自己的版本。用户程序应使用标准的 queue 类,而不
是我们这里定义的这个 Queue 类。
我们自定义的 Queue 类必须能够支持不同类型的对象, 所以将它定义为类模
板。Queue 类将支持的操作是标准 queue 类接口的子集:
• push 操作,在队尾增加一项
• pop 操作,从队头删除一项
• front 操作,返回队头元素的引用
• empty 操作,指出队列中是否有元素
第 16.4 节将介绍怎样实现 Queue 类,这里先定义它的接口:

     template <class Type> class Queue {
     public:
         Queue ();                // default constructor
         Type &front ();          // return element from head of Queue
         const Type &front () const;
         void push (const Type &); // add element to back of Queue
         void pop();              // remove element from head of Queue
         bool empty() const;      // true if no elements in the Queue
     private:
         // ...
     };
类模板也是模板,因此必须以关键字 template 开头,后接模板形参表。

Queue 模板接受一个名为 Type 的模板类型形参。
除了模板形参表外,类模板的定义看起来与任意其他类问相似。类模板可以
定义数据成员、函数成员和类型成员,也可以使用访问标号控制对成员的访问,
还可以定义构造函数和析构函数等等。在类和类成员的定义中,可以使用模板形
参作为类型或值的占位符,在使用类时再提供那些类型或值。
例如,Queue 模板有一个模板类型形参,可以在任何可以使用类型名字的地
方使用该形参。在这个模板定义中,用 Type 指定重载 front 操作的返回类型
以及作为 push 操作的形参类型。
使用类模板
与调用函数模板形成对比,使用类模板时,必须为模板形参显式指定实参:

     Queue<int> qi;                 // Queue that holds ints
     Queue< vector<double> > qc;    // Queue that holds vectors of
doubles
     Queue<string> qs;              // Queue that holds strings

编译器使用实参来实例化这个类的特定类型版本。实质上,编译器用用户提
供的实际特定类型代替 Type,重新编写 Queue 类。在这个例子中,编译器将实
例化三个 Queue 类:第一个用 int 代替 Type,第二个用 vector 代
替 Type,第三个用 string 代替 Type。
Exercises Section 16.1.2
Exercise
16.4:
什么是函数模板?什么是类模板?
Exercise
16.5:
定义一个函数模板,返回两个值中较大的一个。
Exercise
16.6:
类似于我们的 queue 简化版本,编写一个名为 List 的
类模板,作为标准 list 类的简化版本。

16.1.3. 模板形参
像函数形参一样,程序员为模板形参选择的名字没有本质含义。在我们的例
子中,将 compare 的模板类型形参命名为 T,但也可以将它命名为任意名字:

     // equivalent template definition
     template <class Glorp>
     int compare(const Glorp &v1, const Glorp &v2)
     {
         if (v1 < v2) return -1;
         if (v2 < v1) return 1;
         return 0;
     }

该代码定义的 compare 模板与前面一样。
可以给模板形参赋予的唯一含义是区别形参是类型形参还是非类型形参。如
果是类型形参,我们就知道该形参表示未知类型,如果是非类型形参,我们就知
道它是一个未知值。
如果希望使用模板形参所表示的类型或值,可以使用与对应模板形参相同的
名字。例如,compare 函数中所有的 Glorp 引用将在该函数被实例化时确定为
同一类型。

模板形参作用域
模板形参的名字可以在声明为模板形参之后直到模板声明或定义的末尾处
使用。
模板形参遵循常规名字屏蔽规则。与全局作用域中声明的对象、函数或类型
同名的模板形参会屏蔽全局名字:

     typedef double T;
     template <class T> T calc(const T &a, const T &b)
     {
          // tmp has the type of the template parameter T
          // not that of the global typedef
          T tmp = a;
          // ...
          return tmp;
     }

将 T 定义为 double 的全局类型型别名将被名为 T 的类型形参所屏蔽,因

此,tmp 不是 double 型,相反,tmp 的类型是绑定到模板形参的任意类型。
使用模板形参名字的限制
用作模板形参的名字不能在模板内部重用。

     template <class T> T calc(const T &a, const T &b)
     {
         typedef double T; // error: redeclares template parameter T
         T tmp = a;
         // ...
         return tmp;
     }

这一限制还意味着模板形参的名字只能在同一模板形参表中使用一次:

     // error: illegal reuse of template parameter name V
     template <class V, class V> V calc(const V&, const V&) ;

当然, 正如可以重用函数形参名字一样, 模板形参的名字也能在不同模板中重用:

     // ok: reuses parameter type name across different templates
     template <class T> T calc (const T&, const T&) ;
     template <class T> int compare(const T&, const T&) ;

模板声明
像其他任意函数或类一样,对于模板可以只声明而不定义。声明必须指出函
数或类是一个模板:

// declares compare but does not define it
     template <class T> int compare(const T&, const T&) ;

同一模板的声明和定义中,模板形参的名字不必相同。

     // all three uses of calc refer to the same function template
     // forward declarations of the template
     template <class T> T calc(const T&, const T&) ;
     template <class U> U calc(const U&, const U&) ;
     // actual definition of the template
     template <class Type>
     Type calc(const Type& a, const Type& b) { }

每个模板类型形参前面必须带上关键字 class 或 typename,每个非类型形
参前面必须带上类型名字,省略关键字或类型说明符是错误的:

     // error: must precede U by either typename or class
     template <typename T, U> T calc (const T&, const U&) ; 

Exercises Section 16.1.3
Exercise
16.7:
解释下面每个函数模板的定义并指出是否有非法的。改正
所发现的错误。

     (a) template <class T, U, typename V> void f1(T,
U, V) ;
     (b) template <class T> T f2(int &T) ;
     (c) inline template <class T> T foo(T, unsigned
int*) ;
     (d) template <class T> f4 (T, T) ;
     (e) typedef char Ctype ;
         template <typename Ctype> Ctype f5(Ctype
a) ;

Exercise
16.8:
如果有,解释下面哪些声明是错误的说明为什么。

     (a) template <class Type> Type bar(Type, Type) ;
         template <class Type> Type bar(Type,
Type) ;
     (b) template <class T1, class T2> void bar(T1,
T2) ;
         template <class C1, typename C2> void
bar(C1, C2) ;

Exercise
16.9:
编写行为类似于标准库中 find 算法的模板。你的模板应
接受一个类型形参,该形参指定函数形参(一对迭代器)
的类型。使用你的函数在 vector 和 list
中查找给定值。

16.1.4. 模板类型形参
类型形参由关键字 class 或 typename 后接说明符构成。在模板形参表中,这两个关键字具有相同的含义,都指出后面所接的名字表示一个类型。

模板类型形参可作为类型说明符在模板中的任何地方,与内置类型说明符或类类型说明符的使用方式完全相同。具体而言,它可以用于指定返回类型或函数形参类型,以及在函数体中用于变量声明或强制类型转换。

     // ok: same type used for the return type and both parameters
     template <class T> T calc (const T& a, const T& b)
     {
          // ok: tmp will have same type as the parameters & return type
          T tmp = a;
          // ...
          return tmp;
     }

typename 与 class 的区别
在函数模板形参表中,关键字 typename 和 class 具有相同含义,可以互换使用,两个关键字都可以在同一模板形参表中使用:

///
     // ok: no distinction between typename and class in template
parameter list
     template <typename T, class U> calc (const T&, const U&);
 

使用关键字 typename 代替关键字 class 指定模板类型形参也许更为直观,毕竟,可以使用内置类型(非类类型)作为实际的类型形参,而且,typename更清楚地指明后面的名字是一个类型名。但是,关键字 typename 是作为标准
C++ 的组成部分加入到 C++ 中的,因此旧的程序更有可能只用关键字 class。
在模板定义内部指定类型
除了定义数据成员或函数成员之外,类还可以定义类型成员。例如,标准库的容器类定义了不同的类型,如 size_type,使我们能够以独立于机器的方式使用容器。如果要在函数模板内部使用这样的类型,必须告诉编译器我们正在使用的名字指的是一个类型。必须显式地这样做,因为编译器(以及程序的读者)不能通过检查得知,由类型形参定义的名字何时是一个类型何时是一个值。例如,考虑下面的函数:

     template <class Parm, class U>
     Parm fcn(Parm* array, U value)
     {
         Parm::size_type * p; // If Parm::size_type is a type, then a
declaration

                              // If Parm::size_type is an object, then
multiplication
     }

“`
我们知道 size_type 必定是绑定到 Parm 的那个类型的成员,但我们不知道 size_type 是一个类型成员的名字还是一个数据成员的名字,默认情况下,编译器假定这样的名字指定数据成员,而不是类型。如果希望编译器将 size_type 当作类型,则必须显式告诉编译器这样做:

     template <class Parm, class U>
     Parm fcn(Parm* array, U value)
     {
         typename Parm::size_type * p; // ok: declares p to be a pointer
     }

通过在成员名前加上关键字 typename 作为前缀,可以告诉编译器将成员当作类型。通过编写 typename parm::size_type,指出绑定到 Parm 的类型的size_type 成员是类型的名字。当然,这一声明给用实例化 fcn 的类型增加了一个职责:那些类型必须具有名为 size_type 的成员,而且该成员是一个类型。 

如果拿不准是否需要以 typename 指明一个名字是一个类型,那么指定它是个好主意。在类型之前指定 typename 没有害处,因此,即使 typename 是不必要的,也没有关系。
Exercises Section 16.1.4
Exercise
16.10:
声明为 typename 的类型形参与声明为 class 的类型形参有区别吗?区别在哪里?
Exercise
16.11:
何时必须使用 typename?
Exercise
16.12:
编写一个函数模板,接受表示未知类型迭代器的一对值,找出在序列中出现得最频繁的值。
Exercise
16.13:
编写一个函数,接受一个容器的引用并打印该容器的元素。使用容器的 size_type 和 size 成员控制打印元素
的循环。
Exercise
16.14:
重新编写上题的函数,使用从 begin 和 end 返回的迭代器来控制循环。

16.1.5. 非类型模板形参
模板形参不必都是类型。本节将介绍函数模板使用的非类型形参。在介绍了类模板实现的更多内容之后,第 16.4.2 节将介绍类模板的非类型形参。在调用函数时非类型形参将用值代替, 值的类型在模板形参表中指定。 例如,下面的函数模板声明了 array_init 是一个含有一个类型模板形参和一个非类型模板形参的函数模板。 函数本身接受一个形参, 该形参是数组的引用 (第 7.2.4
节):


// initialize elements of an array to zero
template <class T, size_t N> void array_init(T (&parm)[N])
{
for (size_t i = 0; i != N; ++i) {
parm[i] = 0;
}
}

模板非类型形参是模板定义内部的常量值,在需要常量表达式的时候,可使用非类型形参(例如,像这里所做的一样)指定数组的长度。

当调用 array_init 时,编译器从数组实参计算非类型形参的值:
int x[42];
double y[10];
array_init(x); // instantiates array_init(int(&)[42]
array_init(y); // instantiates array_init(double(&)[10]

编译器将为 array_init 调用中用到的每种数组实例化一个 array_init版本。对于上面的程序,编译器将实例化 array_init 的两个版本:第一个实例的形参绑定到 int[42],另一个实例中的形参绑定到 double[10]。
类型等价性与非类型形参对模板的非类型形参而言,求值结果相同的表达式将认为是等价的。下面的两个 array_init 调用引用的是相同的实例——
array_init 《 int, 4

int x[42];
 const int sz = 40;
 int y[sz + 2];
 array_init(x);  // instantiates array_init(int(&)[42])
 array_init(y);  // equivalent instantiation

Exercises Section 16.1.5
Exercise
16.15:
编写可以确定数组长度的函数模板。
Exercise
16.16:
将第 7.2.4 节的 printValues 函数重新编写为可用于
打印不同长度数组内容的函数模板。

16.1.6. 编写泛型程序
编写模板时,代码不可能针对特定类型,但模板代码总是要对将使用的类型做一些假设。例如,虽然 compare 函数从技术上说任意类型都是有效的,但实际上,实例化的版本可能是非法的。

产生的程序是否合法,取决于函数中使用的操作以及所用类型支持的操作。compare 函数有三条语句:
if (v1 < v2) return -1; // < on two objects of type T
if (v2 < v1) return 1; // < on two objects of type T
return 0; // return int; not dependent on T

前两条语句包含隐式依赖于形参类型的代码,if 测试对形参使用 < 操作符,直到编译器看见 compare 调用并且 T 绑定到一个实际类型时,才知道形参的类型,使用哪个 < 操作符完全取决于实参类型。如果用不支持 < 操作符的对象调用 compare,则该调用将是无效的:
Sales_item item1, item2;
// error: no < on Sales_item
cout << compare(item1, item2) << endl;

程序会出错。Sales_item 类型没有定义 < 操作符,所以该程序不能编译。

在函数模板内部完成的操作限制了可用于实例化该函数的类型。程序员的责任是,保证用作函数实参的类型实际上支持所用的任意操作,以及保证在模板使用哪些操作的环境中那些操作运行正常。

编写独立于类型的代码
编写良好泛型代码的技巧超出了本书的范围,但是,有个一般原则值得注意。

编写模板代码时,对实参类型的要求尽可能少是很有益的。

虽然简单,但它说明了编写泛型代码的两个重要原则:
• 模板的形参是 const 引用。

• 函数体中的测试只用 < 比较。
通过将形参设为 const 引用,就可以允许使用不允许复制的类型。大多数类型(包括内置类型和我们已使用过的除 IO 类型之外的所有标准库的类型)都允许复制。但是,也有不允许复制的类类型。将形参设为 const 引用,保证这种类型可以用于 compare 函数,而且,如果有比较大的对象调用 compare,则这个设计还可以使函数运行得更快。一些读者可能认为使用 < 和 > 操作符两者进行比较会更加自然:
// expected comparison
if (v1 < v2) return -1;
if (v1 > v2) return 1;
return 0;

但是,将代码编写为
// expected comparison
if (v1 < v2) return -1;
if (v2 < v1) return 1; // equivalent to v1 > v2
return 0;

可以减少对可用于 compare 函数的类型的要求,这些类型必须支持 <,但不必
支持 >。
Exercises Section 16.1.6
Exercise
16.17:
在第 3.3.2 节的“关键概念”中,我们注意到,C++ 程序员习惯于使用 != 而不用 <,解释这一习惯的基本原
理。
Exercise
16.18:
本节中我们提到应该慎重地编写 compare 中的信息论以避免要求类型同时具有 < 和 > 操作符,另一方面,往往
假定类型既有 == 又有 !=。解释为什么这一看似不一致的处理实际上反映了良好的编程风格。

警告:链接时的编译时错误
一般而言,编译模板时,编译器可能会在三个阶段中标识错误:第一阶段是编译模板定义本身时。 在这个阶段中编译器一般不能发现许多错误,可以检测到诸如漏掉分号或变量名拼写错误一类的语法错误。第二个错误检测时间是在编译器见到模板的使用时。在这个阶段,编译器仍没有很多检查可做。对于函数模板的调用,许多编译器只检查实参的数目和类型是否恰当,编译器可以检测到实参太多或太少,也可以检测到假定类型相同的两个实参是否真地类型相同。对于类模板,编译器可以检测提供的模板实参的正确数目。产生错误的第三个时间是在实例化的时候,只有在这个时候可以发现类型相关的错误。根据编译器管理实例化的方式(将在第 16.3 节讨论),有可能在链接时报告这些错误。重要的是,要认识到编译模板定义的时候,对程序是否有效所知不多。类似地,甚至可能会在已经成功编译了使用模板的每个文件之后出现编译错误。只在实例化期间检测错误的情况很少,错误检测可能发生在链接时。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ZenZenZ

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值