引言:
我们并不总是能够写出对所有可能被实例化的类型都最合适的模板。某些情况下,通用模板定义对于某个类型可能是完全错误的,通用模板定义也许不能编译或者做错误的事情;另外一些情况下,可以利用关于类型的一些特殊知识,编写比从模板实例化来的函数更有效率的函数。
compare函数和 Queue类都是这一问题的好例子:与C风格字符串一起使用进,它们都不能正确工作。
compare函数模板:
template <typename Type> int compare(const Type &v1,const Type &v2) { if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; }
如果用两个constchar*实参调用这个模板定义,函数将比较指针值。它将告诉我们这两个指针在内存中的相对位置,但没有说明与指针所指数组的内容有关的任何事情。
为了能够将compare函数用于字符串,必须提供一个知道怎样比较C风格字符串的特殊定义。这些版本是特化的,这一事实对模板的用户透明。对用户而言,调用特化函数或使用特化类,与使用从通用模板实例化的版本无法区别。
一、函数模板的特化
模板特化:该定义中一个或多个模板实参的实际类型或实际值是指定的。特化形式如下:
1)关键字template后面接一对空的尖括号(<>);
2)再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;
3)函数形参表;
4)函数体。
当模板形参类型绑定到const char* 时,compare函数的特化:
template<> int compare<const char *>(const char *const &v1, const char *const &v2) { return strcmp(v1,v2); }
特化的声明必须与对应的模板相匹配。当调用compare函数的时候,传给它两个字符指针,编译器将调用特化版本。编译器将为任意其他实参类型(包括普通char*)调用泛型版本:
const char *p1 = "Hello",*p2 = "world"; int i1,i2; cout << compare(p1,p2) << endl; cout << compare(i1,i2) << endl;
1、声明模板特化
与任意函数一样,函数模板特化可以声明而无须定义。
template<> int compare<const char *>(const char *const &v1, const char *const &v2);
模板特化必须总是包含空模板形参说明符,即template<>,而且,还必须包含函数形参表。如果可以从函数形参表推断模板实参,则不必显式指定模板实参:
int compare<const char *>(const char *const &v1, const char *const &v2); //Error template<> int compare<const char *>; //OK template<> int compare(const char *const &v1, const char *const &v2); //OK
2、函数重载与模板特化
在特化中省略空的模板形参表template<>会有令人惊讶的结果。如果缺少该特化语法,则结果是声明该函数的重载非模板版本:
template<> int compare(const char *const &v1, const char *const &v2); int compare(const char *const &v1, const char *const &v2); //OK:但是是非模板的重载版本!
当定义非模板函数的时候,对实参应用常规转换;当特化模板的时候,对实参类型不应用转换。在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,如果不完全匹配,编译器将为实参从模板定义实例化一个实例。
3、不是总能检测到重复定义
如果程序由多个文件构成,模板特化的声明必须在使用该特化的每个文件中出现。不能在一些文件中从泛化模板定义实例化一个函数模板,而在其他文件中为同一个模板实参集合特化该函数版本。
【最佳实践】
与其他函数声明一样,应在一个头文件中包含模板特化的声明,然后使用该特化的每个源文件包含该文件!
4、普通作用域规则用于特化
在能够声明或定义特化之前,它所特化的模板的声明必须在作用域中。类似的,在调用模板的这个版本之前,特化的声明必须在作用域中:
template <typename Type> int compare(const Type &v1,const Type &v2) { cout << "generic" << endl; if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; } int main() { int i = compare("Hello","World"); //print “generic” } template<> int compare<const char *>(const char *const &v1, const char *const &v2) { cout << "hah" << endl; return strcmp(v1,v2); }
这个程序有错误,因为在声明特化之前,进行了可以与特化相匹配的一个调用。当编译器看到一个函数调用时,它必须知道这个版本需要特化,否则,编译器将可能从模板定义实例化该函数。
对具有同一模板实参集的同一模板,程序不能既有显式特化又有实例化。
特化出现在对该模板实例的调用之后是错误的。
//P567 习题16.52/53/54 template <typename ValType> size_t count(const vector<ValType> &vec,const ValType &val) { size_t appers = 0; for (typename vector<ValType>::const_iterator iter = vec.begin(); iter != vec.end(); ++iter) { if (*iter == val) { ++ appers; } } return appers; } template <> size_t count(const vector<string> &vec,const string &val); int main() { ifstream inFile("input"); vector<int> ivec; int val; while (inFile >> val) { ivec.push_back(val); } while (cin >> val) { cout << count(ivec,val) << endl; } } template <> size_t count(const vector<string> &vec,const string &val) { size_t appers = 0; for (typename vector<string>::const_iterator iter = vec.begin(); iter != vec.end(); ++iter) { if (*iter == val) { ++ appers; } } return appers; }
二、类模板的特化
当用于C风格字符串时,Queue类具有与compare函数相似的问题。在这种情况下,问题出在push函数中,该函数复制给定值以创建Queue中的新元素。默认情况下,复制C风格字符串只会复制指针,不会复制字符。这种情况下复制指针将出现共享指针在其他环境中会出现的所有问题,最严重的是,如果指针指向动态内存,用户就有可能删除指针所指的数组。
1、定义类特化
为C风格字符串的Queue提供正确行为的一种途径,是为constchar *定义整个类的特化版本:
template<> class Queue<const char *> { public: void push(const char *); void pop() { real_queue.pop(); } bool empty() const { return real_queue.empty(); } std::string front() { return real_queue.front(); } const std::string front() const { return real_queue.front(); } private: Queue<std::string> real_queue; };
这个实现了Queue一个数据元素:string对象的Queue。各个成员将它们的工作委派给这个成员。
Queue类的这个版本没有定义复制控制成员,它唯一的数据成员为类类型,该类类型在被复制、被赋值或被撤销时完成正确的工作。可以使用合成的复制控制成员。
这个Queue类实现了与Queue的模板版本大部分相同但不完全相同的接口,区别在于front成员返回的是string而不是char*,这样做是为了避免必须管理字符数组——如果想要返回指针,就需要字符数组。
值得注意的是:特化可以定义与模板本身完全不同的成员。如果一个特化无法从模板定义某个成员,该特化类型的对象就不能使用该成员。类模板成员的定义不会用于创建显式特化成员的定义。
【最佳实践】
类模板特化应该与它所特化的模板定义相同的接口,否则当用户试图使用未定义的成员时会感到奇怪。
2、类特化定义
在类特化外部定义成员时,成员之前不能加template<>标记。
void Queue<const char *>::push(const char *p) { return real_queue.push(p); }
虽然这个函数几乎没有做什么工作,但它隐式复制了val指向的字符数组。复制是在对real_queue.push的调用中进行的,该调用从constchar* 实参创建了一个新的string对象。constchar * 实参使用了以constchar * 为参数的string构造函数,string构造函数将val所指的数组中的字符复制到未命名的string对象,该对象将被存储在push到 real_queue的元素中。