第十六章 模板和泛型编程
1. 模板定义
定义函数模板:函数模板是一个独立于类型的函数,可作为一种方式,产生函数的特定类型版本。模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。模板形参表不能为空。
// implement strcmp-like generic compare function
// returns 0 if the values are equal, 1 if v1 is larger, -1 if v1 is smaller
template
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
模板形参表:模板形参表示可以在类或函数的定义中使用的类型或值。表示哪个实际类型由编译器根据所用的函数而确定。模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。非类型形参跟在类型说明符之后声明,类型形参跟在关键字 class 或 typename 之后定义,例如,class T 是名为 T 的类型形参,在这里 class 和 typename 没有区别。
编译器将确定用什么类型代替每个类型形参,以及用什么值代替每个非类型形参。推导出实际模板实参后,编译器使用实参代替相应的模板形参产生编译该版本的函数:
cout << compare(1, 0) << endl; // T is int;
string s1 = "hi", s2 = "world";
cout << compare(s1, s2) << endl; // T is string;
inline 函数模板:函数模板可以用与非模板函数一样的方式声明为 inline。说明符放在模板形参表之后、返回类型之前,不能放在关键字 template 之前。
// ok: inline specifier follows template parameter list
template
// error: incorrect placement of inline specifier
inline template
定义类模板:
template
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:
// ...
};
使用类模板时,必须为模板形参显式指定实参:
Queue
Queue< vector
Queue
模板形参作用域:模板形参的名字可以在声明为模板形参之后直到模板声明或定义的末尾处使用。模板形参遵循常规名字屏蔽规则。与全局作用域中声明的对象、函数或类型同名的模板形参会屏蔽全局名字:
typedef double T;
template
{
// tmp has the type of the template parameter T
// not that of the global typedef
T tmp = a;
// ...
return tmp;
}
使用模板形参名字的限制:用作模板形参的名字不能在模板内部重用。
template
{
typedef double T; // error: redeclares template parameter T
T tmp = a;
// ...
return tmp;
}
这一限制还意味着模板形参的名字只能在同一模板形参表中使用一次:
// error: illegal reuse of template parameter name V
template
模板声明:像其他任意函数或类一样,对于模板可以只声明而不定义。声明必须指出函数或类是一个模板:
// declares compare but does not define it
template
同一模板的声明和定义中,模板形参的名字不必相同。
// all three uses of calc refer to the same function template
// forward declarations of the template
template
template
// actual definition of the template
template
Type calc(const Type& a, const Type& b) { /* ... */ }
每个模板类型形参前面必须带上关键字 class 或 typename,每个非类型形参前面必须带上类型名字,省略关键字或类型说明符是错误的:
// error: must precede U by either typename or class
template
typename 与 class 的区别:在函数模板形参表中,关键字 typename 和 class 具有相同含义,可以互换使用,两个关键字都可以在同一模板形参表中使用:
// ok: no distinction between typename and class in template parameter list
template
关键字 typename 是作为标准 C++ 的组成部分加入到 C++ 中的,因此旧的程序更有可能只用关键字 class。
通过在成员名前加上关键字 typename 作为前缀,可以告诉编译器将成员当作类型。默认情况下,编译器假定这样的名字指定数据成员,而不是类型。
template
Parm fcn(Parm* array, U value)
{
typename Parm::size_type * p; // ok: declares p to be a pointer
}
这一声明给用实例化 fcn 的类型增加了一个职责:那些类型必须具有名为 size_type 的成员,而且该成员是一个类型。
非类型模板形参:
在调用函数时非类型形参将用值代替,值的类型在模板形参表中指定。下面的函数模板声明了 array_init 是一个含有一个类型模板形参和一个非类型模板形参的函数模板。函数本身接受一个形参,该形参是数组的引用
// initialize elements of an array to zero
template
{
for (size_t i = 0; i != N; ++i) {
parm[i] = 0;
}
}
int x[42];
double y[10];
array_init(x); // instantiates array_init(int(&)[42]
array_init(y); // instantiates array_init(double(&)[10]
在函数模板内部完成的操作限制了可用于实例化该函数的类型。程序员的责任是,保证用作函数实参的类型实际上支持所用的任意操作,以及保证在模板使用哪些操作的环境中那些操作运行正常。
编写模板代码时,对实参类型的要求尽可能少是很有益的。编写泛型代码的两个重要原则:
1)模板的形参是 const 引用。
2)函数体中的测试只用 < 比较。
链接时的编译时错误:编译模板时,编译器可能会在三个阶段中标识错误:第一阶段是编译模板定义本身时。在这个阶段中编译器一般不能发现许多错误,可以检测到诸如漏掉分号或变量名拼写错误一类的语法错误。第二个错误检测时间是在编译器见到模板的使用时。在这个阶段,编译器仍没有很多检查可做。对于函数模板的调用,许多编译器只检查实参的数目和类型是否恰当,编译器可以检测到实参太多或太少,也可以检测到假定类型相同的两个实参是否真地类型相同。对于类模板,编译器可以检测提供的模板实参的正确数目。产生错误的第三个时间是在实例化的时候,只有在这个时候可以发现类型相关的错误。根据编译器管理实例化的方式,有可能在链接时报告这些错误。要认识到编译模板定义的时候,对程序是否有效所知不多。类似地,甚至可能会在已经成功编译了使用模板的每个文件之后出现编译错误。只在实例化期间检测错误的情况很少,错误检测可能发生在链接时。
2. 实例化
类模板不定义类型,只有特定的实例才定义了类型。例如,Queue 不是类型,而 Queue
模板类型形参可以用作一个以上函数形参的类型。在这种情况下,模板类型推断必须为每个对应的函数实参产生相同的模板实参类型。如果推断的类型不匹配,则调用将会出错:
template
int compare(const T& v1, const T& v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
int main()
{
short si;
// error: cannot instantiate compare(short, int)
// must be: compare(short, short) or
// compare(int, int)
compare(si, 1024);
return 0;
}
如果 compare 的设计者想要允许实参的常规转换,则函数必须用两个类型形参来定义,但是,比较那些类型的值的 < 操作符必须存在:
// argument types can differ, but must be compatible
template
int compare(const A& v1, const B& v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
类型形参的实参的受限转换:一般而论,不会转换实参以匹配已有的实例化,相反,会产生新的实例。除了产生新的实例化之外,编译器只会执行两种转换:
1)const 转换:接受 const 引用或 const 指针的函数可以分别用非 const 对象的引用或指针来调用,无须产生新的实例化。如果函数接受非引用类型,形参类型实参都忽略 const,即,无论传递 const 或非 const 对象给接受非引用类型的函数,都使用相同的实例化。
2)数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针。
例如,考虑对函数 fobj 和 fref 的调用。fobj 函数复制它的形参,而 fref 的形参是引用:
template
template
T fref(const T&, const T&); // reference arguments
string s1("a value");
const string s2("another value");
fobj(s1, s2); // ok: calls f(string, string), const is ignored
fref(s1, s2); // ok: non const object s1 converted to const reference
int a[10], b[42];
fobj(a, b); // ok: calls f(int*, int*)
fref(a, b); // error: array types don't match; arguments aren't converted to pointers
在第二种情况中,将传递不同长度的数组实参。fref 的调用是非法的,当形参为引用时,数组不能转换为指针,a 和 b 的类型不匹配,所以调用将出错。
类型转换的限制只适用于类型为模板形参的那些实参。用普通类型定义的形参可以使用常规转换。
模板实参推断与函数指针:可以使用函数模板对函数指针进行初始化或赋值,这样做的时候,编译器使用指针的类型实例化具有适当模板实参的模板版本。
template
int (*pf1) (const int&, const int&) = compare;
pf1 的类型是一个指针,指向“接受两个 const int& 类型形参并返回 int 值的函数”,形参的类型决定了 T 的模板实参的类型,T 的模板实参为 int 型,指针 pf1 引用的是将 T 绑定到 int 的实例化。
获取函数模板实例化的地址的时候,上下文必须是这样的:它允许为每个模板形参确定唯一的类型或值:
void func(int(*) (const string&, const string&));
void func(int(*) (const int&, const int&));
func(compare); // error: which instantiation of compare?
函数模板的显式实参:在某些情况下,不可能推断模板实参的类型。当函数的返回类型必须与形参表中所用的所有类型都不同时,最常出现这一问题。在这种情况下,有必要覆盖模板实参推断机制,并显式指定为模板形参所用的类型或值。
template
解决这一问题的一个办法,可能是强制 sum 的调用者将较小的类型强制转换为希望作为结果使用的类型:
// ok: now either T or U works as return type
int i; short s;
sum(static_cast
在返回类型中使用类型形参:指定返回类型的一种方式是引入第三个模板形参,它必须由调用者显式指定:
template
T1 sum(T2, T3);
没有实参的类型可用于推断 T1 的类型,相反,调用者必须在每次调用 sum 时为该形参显式提供实参。在以逗号分隔、用尖括号括住的列表中指定显式模板实参。显式模板类型的列表出现在函数名之后、实参表之前:
long val3 = sum
这一调用显式指定 T1 的类型,编译器从调用中传递的实参推断 T2 和 T3 的类型。
显式模板实参从左至右对应模板形参相匹配,假如可以从函数形参推断,则结尾(最右边)形参的显式模板实参可以省略。如果这样编写 sum 函数:
template
T3 alternative_sum(T2, T1);
则总是必须为所有三个形参指定实参:
// error: can't infer initial template parameters
long val3 = alternative_sum
// ok: All three parameters explicitly specified
long val2 = alternative_sum
显式实参与函数模板的指针:通过使用显式模板实参能够消除二义性:
template
// overloaded versions of func; each take a different function pointer type
void func(int(*) (const string&, const string&));
void func(int(*) (const int&, const int&));
func(compare
3. 模板编译模型
当编译器看到模板定义的时候,它不立即产生代码。只有在看到用到模板时,如调用了函数模板或调用了类模板的对象的时候,编译器才产生特定类型的模板实例。
一般而言,当调用函数的时候,编译器只需要看到函数的声明。类似地,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。模板则不同:要进行实例化,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要那些通常放在源文件中的代码。
标准 C++ 为编译模板代码定义了两种模型。在两种模型中,构造程序的方式很大程度上是相同的:类定义和函数声明放在头文件中,而函数定义和成员定义放在源文件中。两种模型的不同在于,编译器怎样使用来自源文件的定义。
包含编译模型:编译器必须看到用到的所有模板的定义。一般而言,可以通过在声明函数模板或类模板的头文件中添加一条 #include 指示使定义可用,该 #include 引入了包含相关定义的源文件。
分别编译模型:编译器会为我们跟踪相关的模板定义。但是,我们必须让编译器知道要记住给定的模板定义,可以使用 export 关键字来做这件事。export 关键字能够指明给定的定义可能会需要在其他文件中产生实例化。在一个程序中,一个模板只能定义为导出一次。编译器在需要产生这些实例化时计算出怎样定位模板定义。export 关键字不必在模板声明中出现。
export template
Type sum(Type t1, Type t2) /* ...*/
这个函数模板的声明像通常一样应放在头文件中,声明不必指定 export。相反,应该在类的实现文件中使用 export:
// class template header goes in shared header file
template
// Queue.ccimplementation file declares Queue as exported
export template
#include "Queue.h"
// Queue member definitions
导出类的成员将自动声明为导出的。也可以将类模板的个别成员声明为导出的,在这种情况下,关键字 export 不在类模板本身指定,而是只在被导出的特定成员定义上指定。导出成员函数的定义不必在使用成员时可见。任意非导出成员的定义必须像在包含模型中一样对待:定义应放在定义类模板的头文件中。
4. 类模板成员(Queue 类模板的具体实现)【Coding】
5. 一个泛型句柄类
Handle 类行为类似于指针:复制 Handle 对象将不会复制基础对象,复制之后,两个 Handle 对象将引用同一基础对象。要创建 Handle 对象,用户需要传递属于由 Handle 管理的类型(或从该类型派生的类型)的动态分配对象的地址,从此刻起,Handle 将“拥有”这个对象。而且,一旦不再有任意 Handle 对象与该对象关联,Handle 类将负责删除该对象。
template
public:
// unbound handle
Handle(T *p = 0): ptr(p), use(new size_t(1)) { }
// overloaded operators to support pointer behavior
T& operator*();
T* operator->();
const T& operator*() const;
const T* operator->() const;
// copy control: normal pointer behavior, but last Handle deletes the object
Handle(const Handle& h): ptr(h.ptr), use(h.use)
{ ++*use; }
Handle& operator=(const Handle&);
~Handle() { rem_ref(); }
private:
T* ptr; // shared object
size_t *use; // count of how many Handle spointto *ptr
void rem_ref()
{ if (--*use == 0) { delete ptr; delete use; } }
};
template
inline Handle
{
++*rhs.use; // protect against self-assignment
rem_ref(); // decrement use count and delete pointers if needed
ptr = rhs.ptr;
use = rhs.use;
return *this;
}
template
{
if (ptr) return *ptr;
throw std::runtime_error
("dereference of unbound Handle");
}
template
{
if (ptr) return ptr;
throw std::runtime_error
("access through unbound Handle");
}
分配一个 int 对象,并将一个 Handle 对象绑定到新分配的 int 对象来说明 Handle 的行为:
{ // new scope
// user allocates but must not delete the object to which the Handle is attached
Handle
{ // new scope
Handle
cout << *hp << " " << *hp2 << endl; // prints 42 42
*hp2 = 10; // changes value of shared underlying int
} // hp2 goes out of scope; use count is decremented
cout << *hp << endl; // prints 10
} // hp goes out of scope; its destructor deletes the int
即使是 Handle 的用户分配了 int 对象,Handle 析构函数也将删除它。在外层代码块末尾最后一个 Handle 对象超出作用域时,删除该 int 对象。为了访问基础对象,应用了 Handle 的 * 操作符,该操作符返回对基础 int 对象的引用。
使用 Handle 对象对指针进行使用计数:
class Sales_item {
public:
// default constructor: unbound handle
Sales_item(): h() { }
// copy item and attach handle to the copy
Sales_item(const Item_base &item): h(item.clone()) { }
// no copy control members: synthesized versions work
// member access operators: forward their work to the Handle class
const Item_base& operator*() const { return *h; }
const Item_base* operator->() const
{ return h.operator->(); }
private:
Handle
};
基于 Handle 的 Sales_item 版本有一个数据成员,该数据成员是关联传给构造函数的 Item_base 对象的副本上的 Handle 对象。因为 Sales_item 的这个版本没有指针成员,所以不需要复制控制成员,Sales_item 的这个版本可以安全地使用合成的复制控制成员。管理使用计数和相关 Item_base 对象的工作在 Handle 内部完成。
double Basket::total() const
{
double sum = 0.0; // holds the running total
/* find each set of items with the same isbn and calculate
* the net price for that quantity of items
* iter refers to first copy of each book in the set
* upper_boundrefers to next element with a different isbn
*/
for (const_iter iter = items.begin();
iter != items.end();
iter = items.upper_bound(*iter))
{
// we know there's at least one element with this key in the Basket
// virtual call to net_priceapplies appropriate discounts, if any
sum += (*iter)->net_price(items.count(*iter));
}
return sum;
}
6. 模板特化(高级主题)
7. 重载与函数模板
函数模板可以重载:可以定义有相同名字但形参数目或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数。
如果重载函数中既有普通函数又有函数模板,确定函数调用的步骤如下:
1)为这个函数名建立候选函数集合,包括:
a. 与被调用函数名字相同的任意普通函数.
b. 任意函数模板实例化,在其中,模板实参推断发现了与调用中所用函数实参相匹配的模板实参.
2) 确定哪些普通函数是可行的(如果有可行函数的话)。候选集合中的每个模板实例都是可行的,因为模板实参推断保证函数可以被调用.
3) 如果需要转换来进行调用,根据转换的种类排列可靠函数,记住,调用模板函数实例所允许的转换是有限的.
a. 如果只有一个函数可选,就调用这个函数.
b. 如果调用有二义性,从可行函数集合中去掉所有函数模板实例.
4) 重新排列去掉函数模板实例的可行函数.
a. 如果只有一个函数可选,就调用这个函数.
b. 否则,调用有二义性.
// compares two objects
template
// compares elements in two sequences
template
// plain functions to handle C-style character strings
int compare(const char*, const char*);
可以在不同类型上调用这些函数:
// calls compare(const T&, const T&) with T bound to int
compare(1, 0);
// calls compare(U, U, V), with U and V bound to vector
vector
compare(ivec1.begin(), ivec1.end(), ivec2.begin());
int ia1[] = {0,1,2,3,4,5,6,7,8,9};
// calls compare(U, U, V) with U bound to int*
// and V bound to vector
compare(ia1, ia1 + 10, ivec1.begin());
// calls the ordinary function taking const char* parameters
const char const_arr1[] = "world", const_arr2[] = "hi";
compare(const_arr1, const_arr2);
// calls the ordinary function taking const char* parameters
char ch_arr1[] = "world", ch_arr2[] = "hi";
compare(ch_arr1, ch_arr2);
转换与重载的函数模板:
char *p1 = ch_arr1, *p2 = ch_arr2;
compare(p1, p2);
在这个例子中,将 char* 绑定到 T 的函数模板与该调用完全匹配。普通版本仍然需要从 char* 到 const char* 的转换,所以优先选择函数模板.
当匹配同样好时,非模板版本优先.
定义函数模板特化几乎总是比使用非模板版本更好.