C++Primer学习笔记(十二)模板和泛型编程
所谓泛型编程就是以独立于任何特定类型的方式编写代码。使用泛型程序时,我们需要提供具体程序实例所操作的类型或值。
模板是泛型编程的基础。
模板是创建类或函数的蓝图或公式。
16.1. 模板定义
C++-模板的声明和实现为何要放在头文件中
定义函数模板
我们可以不用为每个类型定义一个新函数,而是只定义一个函数模板(function template)。
模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。
模板形参表不能为空。
模板形参表
模板形参表很像函数形参表,函数形参表定义了特定类型的局部变量但并不初始化那些变量,在运行时再提供实参来初始化形参。
模板形参表示可以在类或函数的定义中使用的类型或值。
模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。
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&);
定义类模板
我们自定义的 Queue 类必须能够支持不同类型的对象,所以将它定义为类模板。
实现 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<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<double> 代替 Type,第三个用 string 代替 Type。
模板形参
可以给模板形参赋予的唯一含义是区别形参是类型形参还是非类型形参。如果是类型形参,我们就知道该形参表示未知类型,如果是非类型形参,我们就知道它是一个未知值。
如果希望使用模板形参所表示的类型或值,可以使用与对应模板形参相同的名字。
模板形参作用域
模板形参的名字可以在声明为模板形参之后直到模板声明或定义的末尾处使用。
模板形参遵循常规名字屏蔽规则。与全局作用域中声明的对象、函数或类型同名的模板形参会屏蔽全局名字
使用模板形参名字的限制
用作模板形参的名字不能在模板内部重用(模板形参的名字只能在同一模板形参表中使用一次)。
模板形参的名字也能在不同模板中重用
模板声明
像其他任意函数或类一样,对于模板可以只声明而不定义。声明必须指出函数或类是一个模板
同一模板的声明和定义中,模板形参的名字不必相同
每个模板类型形参前面必须带上关键字 class 或 typename,每个非类型形参前面必须带上类型名字,省略关键字或类型说明符是错误的
模板类型形参
模板类型形参可作为类型说明符在模板中的任何地方,与内置类型说明符或类类型说明符的使用方式完全相同。
它可以用于指定返回类型或函数形参类型,以及在函数体中用于变量声明或强制类型转换。
在模板定义内部指定类型
通过在成员名前加上关键字 typename 作为前缀,可以告诉编译器将成员当作类型。
如果拿不准是否需要以 typename 指明一个名字是一个类型,那么指定它是个好主意。在类型之前指定 typename 没有害处,因此,即使 typename 是不必要的,也没有关系。
非类型模板形参
在调用函数时非类型形参将用值代替,值的类型在模板形参表中指定。
模板非类型形参是模板定义内部的常量值,在需要常量表达式的时候,可使用非类型形参指定数组的长度。
类型等价性与非类型形参
对模板的非类型形参而言,求值结果相同的表达式将认为是等价的。
编写泛型程序
编写模板时,代码不可能针对特定类型,但模板代码总是要对将使用的类型做一些假设。
产生的程序是否合法,取决于函数中使用的操作以及所用类型支持的操作。
编写独立于类型的代码
编写模板代码时,对实参类型的要求尽可能少是很有益的。
它说明了编写泛型代码的两个重要原则:
• 模板的形参是 const 引用。
• 函数体条件判断只用 < 比较。
警告:链接时的编译时错误
编译模板时,编译器可能会在三个阶段中标识错误:
第一阶段是编译模板定义本身时。
第二个错误检测时间是在编译器见到模板的使用时。
产生错误的第三个时间是在实例化的时候,只有在这个时候可以发现类型相关的错误
编译模板定义的时候,对程序是否有效所知不多。
16.2. 实例化
模板是一个蓝图,它本身不是类或函数。编译器用模板产生指定的类或函数的特定类型版本。产生模板的特定类型实例的过程称为实例化,这个术语反映了创建模板类型或模板函数的新“实例”的概念。
模板在使用时将进行实例化,类模板在引用实际模板类类型时实例化,函数模板在调用它或用它对函数指针进行初始化或赋值时实例化。
类的实例化
类模板的每次实例化都会产生一个独立的类类型。为 int 类型实例化的 Queue 与任意其他 Queue 类型没有关系,对其他Queue 类型成员也没有特殊的访问权。
类模板形参是必需的
想要使用类模板,就必须显式指定模板实参
类模板不定义类型,只有特定的实例才定义了类型。特定的实例化是通过提供模板实参与每个模板形参匹配而定义的。模板实参在用逗号分隔并用尖括号括住的列表中指定:
Queue<int> qi; // ok: defines Queue that holds ints
Queue<string> qs; // ok: defines Queue that holds strings
用模板类定义的类型总是模板实参。
函数模板实例化
模板实参推断
要确定应该实例化哪个函数,编译器会查看每个实参。如果相应形参声明为类型形参的类型,则编译器从实参的类型推断形参的类型。
从函数实参确定模板实参的类型和值的过程叫做模板实参推断。
多个类型形参的实参必须完全匹配
模板类型形参可以用作一个以上函数形参的类型。在这种情况下,模板类型推断必须为每个对应的函数实参产生相同的模板实参类型。如果推断的类型不匹配,则调用将会出错
想要允许实参的常规转换,则函数必须用两个类型形参来定义:
// argument types can differ, but must be compatible
template <typename A, typename B>
int compare(const A& v1, const B& v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
类型形参的实参的受限转换
一般而论,不会转换实参以匹配已有的实例化,相反,会产生新的实例。除了产生新的实例化之外,编译器只会执行两种转换:
• const 转换:接受 const 引用或 const 指针的函数可以分别用非 const对象的引用或指针来调用,无须产生新的实例化。如果函数接受非引用类型,形参类型实参都忽略 const,即,无论传递 const 或非 const 对象给接受非引用类型的函数,都使用相同的实例化。
• 数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针。
应用于非模板实参的常规转换
类型转换的限制只适用于类型为模板形参的那些实参。
模板实参推断与函数指针
可以使用函数模板对函数指针进行初始化或赋值,这样做的时候,编译器使用指针的类型实例化具有适当模板实参的模板版本。
获取函数模板实例化的地址的时候,上下文必须是这样的:它允许为每个模板形参确定唯一的类型或值。
如果不能从函数指针类型确定模板实参,就会出错。
函数模板的显式实参
在某些情况下,不可能推断模板实参的类型。当函数的返回类型必须与形参表中所用的所有类型都不同时,最常出现这一问题。在这种情况下,有必要覆盖模板实参推断机制,并显式指定为模板形参所用的类型或值。
指定显式模板实参
在返回类型中使用类型形参
指定返回类型的一种方式是引入第三个模板形参,它必须由调用者显式指定:
// T1 cannot be deduced: it doesn't appear in the function parameter list
template <class T1, class T2, class T3>
T1 sum(T2, T3);
显式实参与函数模板的指针
可以使用显式模板实参的另一个例子是第 16.2.1 节中有二义性程序,通过使用显式模板实参能够消除二义性
16.3. 模板编译模型
当编译器看到模板定义的时候,它不立即产生代码。只有在看到用到模板时,如调用了函数模板或调用了类模板的对象的时候,编译器才产生特定类型的模板实例。
模板编译和普通的编译不同,它是在使用实例化时编译器才生成代码,为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义,
因此和非模板代码不同,模板的头文件通常既包括声明又包括定义。
包含编译模型
在包含编译模型中,编译器必须看到用到的所有模板的定义。
一般而言,可以通过在声明函数模板或类模板的头文件中添加一条 #include 指示使定义可用,该#include 引入了包含相关定义的源文件
这一策略使我们能够保持头文件和实现文件的分享,但是需要保证编译器在编译使用模板的代码时能看到两种文件。
分别编译模型
在分别编译模型中,编译器会为我们跟踪相关的模板定义。但是,我们必须让编译器知道要记住给定的模板定义,可以使用 export 关键字来做这件事。
对类模板使用 export 更复杂一些。通常,类声明必须放在头文件中,头文件中的类定义体不应该使用关键字 export,如果在头文件中使用了 export,则该头文件只能被程序中的一个源文件使用。
警告:类模板中的名字查找
设计者的责任是,保证所有不依赖于模板形参的名字在模板本身的作用域中定义。
模板用户的责任是,保证与用来实例化模板的类型相关的所有函数、类型和操作符的声明可见。
16.4. 类模板成员
模板作用域中模板类型的引用
通常,当使用类模板的名字的时候,必须指定模板形参。这一规则有个例外:在类本身的作用域内部,可以使用类模板的非限定名。
类模板成员函数
类模板成员函数的定义具有如下形式:
• 必须以关键字 template 开关,后接类的模板形参表。
• 必须指出它是哪个类的成员。
• 类名必须包含其模板形参。
类模板成员函数的实例化
类模板的成员函数本身也是函数模板。
与其他函数模板不同的是,在实例化类模板成员函数的进修,编译器不执行模板实参推断,相反,类模板成员函数的模板形参由调用该函数的对象的类型确定。
对象的模板实参能够确定成员函数模板形参,这一事实意味着,调用类模板成员函数比调用类似函数模板更灵活。用模板形参定义的函数形参的实参允许进行常规转换
何时实例化类和成员
类模板的成员函数只有为程序所用才进行实例化。如果某函数从未使用,则不会实例化该成员函数。
用于实例化模板的类型只需满足实际使用的操作的要求。
非类型形参的模板实参
非类型模板实参必须是编译时常量表达式。
类模板中的友元声明
在类模板中可以出现三种友元声明,每一种都声明了与一个或多个实体友元关系:
1. 普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数。
2. 类模板或函数模板的友元声明,授予对友元所有实例的访问权。
3. 只授予对类模板或函数模板的特定实例的访问权的友元声明。
特定的模板友元关系
除了将一个模板的所有实例设为友元,类也可以只授予对特定实例的访问权
template <class T> class Foo2;
template <class T> void templ_fcn2(const T&);
template <class Type> class Bar {
// grants access to a single specific instance parameterized by char*
friend class Foo2<char*>;
friend void templ_fcn2<char*>(char* const &);
// ...
};
声明依赖性
当授予对给定模板的实例的访问权时候,在作用域中不需要存在该类模板或函数模板的声明。实质上,编译器将友元声明也当作类或函数的声明对待。
想要限制对特定实例化的友元关系时,必须在可以用于友元声明之前声明类或函数
如果没有事先告诉编译器该友元是一个模板,则编译器将认为该友元是一个普通非模板类或非模板函数。
成员模板
任意类(模板或非模板)可以拥有本身为类模板或函数模板的成员,这种成员称为成员模板,成员模板不能为虚。
定义成员模板
模板成员声明看起来像任意模板的声明一样
成员声明的开关是自己的模板形参表。
在类外部定义成员模板
像非模板成员一样,成员模板可以定义在包含它的类或类模板定义的内部或外部。
当成员模板是类模板的成员时,它的定义必须包含类模板形参以及自己的模板形参。
成员模板遵循常规访问控制
成员模板遵循与任意其他类成员一样的访问规则。如果成员模板为私有的,则只有该类的成员函数和友元可以使用该成员模板。
成员模板和实例化
与其他成员一样,成员模板只有在程序中使用时才实例化。
类模板的成员模板的实例化比类模板的普通成员函数的实例化要复杂一点。成员模板有两种模板
形参:由类定义的和由成员模板本身定义的。类模板形参由调用函数的对象的类型确定,成员定义的模板形参的行为与普通函数模板一样。