C++模板详解
一、模板具体化:
函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int或double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)。
创建模板,关键字template和typename是必需的,除非可以使用关键字class代替typename。另外,必须使用尖括号。如下程序所示:
template <typename T> void swap(T &a, T &b) { T temp; temp = a; a = b; b = temp; }
注意:模板并非函数定义,但使用了int的模板实例是函数定义。
(1)隐式实例化:
最初,编译器只能通过隐式实例化,来使用模板生成函数定义,这也是我们最常用的方法;如可以像下面这样使用上面定义的函数模板:
short a, b; swap(a, b); // T 为 short 类型 int c, d; swap(c, d); // T 为 int 类型
使用上面的例子程序,我们可以交换两个同类型(int,double……)的值,当如果T为数组、指针或者结构,那么编写的模板函数就无法处理这些类型了,一种方案是重载C++运算符;另一种方案是,为特定类型提供具体化的模板定义,下面就介绍第二种方案:
(2)显式实例化:
现在C++还允许显式实例化(explicit instrantiation)。这意味着可以直接命令编译器创建特定的实例,如swap<int>()。其语法是,声明所需的种类——用<>符号指示类型,并在声明前加上关键字template:
template void swap<int>(int, int); // explicit instrantiation
实现了这种特性的编译器看到上述声明后,将使用swap()模板生成一个使用int类型的实例。也就是说,该声明的意思是"使用swap()模板生成int类型的函数定义。"
(3)显式具体化:
与显式实例化不同的是,显式具体化使用下面两个等价的声明之一:
template <> void swap<int> (int &, int &); //explicit specialization template <> void swap (int &, int &); //explicit specialization
显式实例化和显式具体化区别在于:这些声明的意思是“不要使用swap()模板来生成函数定义,而应使用专门为int类型显式地定义的函数定义。”这些原型必须有自己的函数定义。显式具体化声明在关键字template后包含<>,而显式实例化没有。
警告:试图在同一个文件中(或转换单元)中使用同一种类型的显式实例化和显式具体化 将出错。
隐式实例化、显式实例化和显式具体化统称为具体化(specialization)。它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述。
引入显式实例化之后,必须使用新的语法——在声明中使用前缀template和template<>,以区分显式实例化和显式具体化。通常,功能越多,语法规则也越多。
下面的代码总结了这些概念:
…… struct job {}; template<typename T> void swap(T &, T &); // template prototype 模板 template<> void swap<job>(job &, job &); // explicit specialization for job 模板具体化 int main(void) { template void swap<char>(char &, char &); // explicit instrantiation for char 模板 显式实例化 short a, b; …… swap(a, b); //implicit template instantiation for short(启用一个模板) job n, m; …… swap(n, m); // use explicit specialization for job(模板具体化,不启用模板) char g, h; …… swap(g, h); // use explicit template instantiation for char(启用另一个模板) …… }
(4)部分具体化:
C++还允许部分具体化(partial speciazation),即部分限制模板的通用性。例如,部分具体化可以给类型参数之一指定具体的类型:
// general template template <typename T1, typename T2> class Pair { …… }; // specialization with T2 set to int template <typename T1> class Pair<T1, int> { …… };
关键字template后面的<>声明的是没有被具体化的类型参数。因此,上述第二个声明将T2具体化为int,但T1保持不变。注意,如果指定所有的类型,则<>内将为空,这将导致显式具体化:
// specialization with T1 and T2 set to int template <> class Pair<int, int> { …… };
如果有多个模板可供选择,编译器将使用具体化程度最高的模板。
下面是模板部分具体化在luaTinker中的实际应用例子:
// ------------------------------------------------------------------------------
/*** 以下代码参考了 luaTinker 中 关于模板的使用 **/
#include <iostream> using namespace std; // "if_<bool, A, B>":类型选择模板(使用模板来实现,根据参数不同匹配不同的模板) template<bool C, typename A, typename B> struct if_ {}; // 模板具体化(相对于一般模板,有更高的优先调用级;如果和具体化模板匹配,则调用具体化模板) // 模板的部分具体化(模板的参数依然是三个,只不过第一个参数被默认指定而已) template<typename A, typename B> struct if_<true, A, B> {typedef A type;}; // first place template<typename A, typename B> struct if_<false, A, B> {typedef B type;}; // second place int main() { if_<true, char, double>::type bb; // 直接匹配 first place 位置部分具体化模板函数 cout << " the sizeof(bb) is = " << sizeof(bb) << endl; if_<false, char, double>::type cc; // 直接匹配 second place 位置部分具体化模板函数 cout << " the sizeof(cc) is = " << sizeof(cc) << endl; /*下面使用两个参数会报 参数不匹配 错误 * if_<char, int>::type dd; * cout << " the sizeof(dd) is = " << sizeof(dd) << endl; */ return 0; } // output ------------------------------------------------------------------------- the sizeof(bb) is = 1 the sizeof(cc) is = 8 请按任意键继续. . .
二、编译器选择使用哪个函数版本:
对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)。详细解释这个策略将需要将近一章的篇幅,因此这里我们只是大致了解一下这个过程是如何进行的。
(1)创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
(2)使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式的转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。
(3)确定是否有最佳的可行函数。如果有,则使用它;否则,该函数调用出错。
三、tuple 和 可变参数模板(C++11新标准添加功能):
【这部分转自】http://www.cnblogs.com/hujian/archive/2012/02/23/2364190.html
C++11中引入的tuple是一个N元组。它相当于有N个成员的结构体,只不过这个结构体的成员都是匿名的。tuple中有两个特殊的函数,一个是head(),用于获取第一个成员的值,另一个是tail(),用于获取剩下所有成员的值,tail()本身又是一个tuple。这样,如果我们想取tuple中第二个成员的值,则可以先取tail()的值,再取tail()的head()的值。当然,这样使用的话比较麻烦,所以C++ 11提供了get函数通过索引来获取tuple中某个成员的值。另外,通过make_tuple可以很方便地构造一个tuple对象。有关tuple使用的例子可以参考下面的代码。
tuple<int, char, string> tupInfo(10, 'A', "hello world"); int a = tupInfo.head(); int a2 = tupInfo.tail().head(); tuple<char, string> tupTail = tupInfo.tail(); int b = get<0>(tupInfo); char c = get<1>(tupInfo); string s = get<2>(tupInfo); auto tupInfo2 = make_tuple(5, 'B', string("C++ 11"), 4.6);
前面说过,tuple是一个N元组,而N的个数是没有限制的,也就是说,tuple可以包含0个、1个、2个或更多的元素,每个元素的类型则通过模板参数指定。那么,tuple是如何做到这些的呢?答案是可变参数模板。学习C++的人应当对printf函数都非常熟悉,printf的一个特点就是它的参数个数是可变的。而在C++ 11中,则允许模板的参数个数也是可变的。下面是一个模板参数可变的函数模板,用于获取传入的参数的个数。
template<typename... Args> UINT GetParameterCount(Args... args) { return sizeof...(args); }
可以看到,可变参数模板使用typename再加...来表示模板参数包,使用Args再加...来表示函数参数包。上面代码中的sizeof...专门用于获取函数参数包中参数的个数,它的参数必须是一个函数参数包类型的对象。熟悉了可变参数模板的基本语法后,下面我们使用它来编写一个Print函数,该函数的参数个数和类型都是可变的,它简单地输出传入的各个参数的值,值之间用逗号进行分割,并在输出最后一个参数的值后自动换行。
template<typename T> void Print(T value) { cout << value << endl; } template<typename Head, typename... Rail> void Print(Head head, Rail... rail) { cout << head << ","; Print(rail...); // 递归调用可变参数模板 } int main(int argc, char *argv[]) { Print(1); // 输出:1 Print(1, "hello"); // 输出:1,Hello Print(1, "hello", 'H'); // 输出:1,Hello,H return 0; }
在上面的代码中,我们先定义了一个只有一个模板参数的函数模板,它简单地输出传入的参数的值。然后又定义了一个可变参数的函数模板,它输出第一个参数的值,然后递归地调用自己。注意rail...这种写法,它表示将函数参数包分割成一个一个的参数,并传入Print中。这样,函数参数包中的第一个参数传递给head,剩余的参数又重新构成一个函数参数包传递给rail。当递归调用到函数参数包中只有一个参数时,则会调用只有一个模板参数的Print函数。
下图是对可变参数模板一个调用过程中参数传递变化的图解:
四、模板类与友元
模板类声明也可以有友元。模板的友元分3类:
非模板 友元。(类模板具体化:非模板友元 == n:1)
约束模板 友元,即友元的类型取决于类被实例化时的类型。(类模板具体化:非模板友元 == 1:1)
非约束模板 友元,即友元的所有具体化都是类的每一个具体化的友元。(类模板具体化:非模板友元 == 1:n)
(1)模板类的非模板友元函数(类模板具体化:非模板友元 == n:1)
在模板类中将一个常规函数声明为友元:
template <class T> class HasFriend { public: friend void counts(); // }
上述声明使counts()函数成为模板所有实例化的友元。例如,它将是类HasFriend<int>和hasFriend<string>的友元。
counts()函数不是通过对象调用的(它是友元,不是成员函数),也没有对象参数,那么它如何访问HasFriend对象呢?有很多种可能性。它可以访问全局对象;可以使用全局指针访问非全局对象;可以创建自己的对象;可以访问独立于对象的模板类的静态数据成员。
假设 要为友元函数提供 模板类参数,可以如下所示来进行友元声明吗?
friend void report(HasFriend &);
答案是不可以。原因是不存在HasFriend这样的对象,而只有特定的具体化,如HasFriend<short>。要提供模板类参数,必须指明具体化。例如,可以这样做:
template <class T> class HasFriend { friend void report(HasFriend<T> &); …… };
(2) 模板类的约束模板友元函数(类模板具体化:非模板友元 == 1:1)
使友元函数本身成为模板,具体地说,为约束模板友元作准备,要使类的每一个具体化都获得与友元匹配的具体化。包含以下3步。
1. 在类定义的前面声明每个模板函数。
template<typename T> void counts(); template<typename T> void report(T &);
2. 在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化(带<>尖括号):
template<typename TT> class HasFriendT { …… friend void counts<TT>(); friend void report<>(HasFriendT<TT> &); };
声明中的<>指出这是模板具体化。对于report(),<>可以为空,因为可以从函数参数推断出如下模板类型参数:
HasFriendT<TT>
然而,也可以使用:
report<HasFriendT<TT> >(HasFriendT<TT> &)
但counts()函数没有参数,因此必须使用模板参数语法(<TT>)来指明其具体化还需要注意的是,TT是HasFriendT类的参数类型。
3. 程序必须满足的第三个要求是,为友元提供模板定义,也就是友元模板的实现函数体。
template<typename T> void counts() { cout << "template size :" << sizeof(HasFriendT<T>) << ";" ; cout << "template counts(): " << HasFriendT<T>::item << endl; }
(3)模板类的非约束模板友元函数(类模板具体化:非模板友元 == 1:n)
前一节中的约束模板友元函数是在类外面声明的模板的具体化,int类具体化获得int函数具体化,依此类推。
通过在类内部声明模板,可以创建非约束模板友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数是不同的:
template<typename T> class ManyFriend { …… template <typename C, typename D> friend void show2(C &, D &); };
下面的程序是一个使用非约束友元的例子。其中,函数定义show2(hfi1, hfi2)与下面的具体化匹配:
void show2<ManyFriend<int> &, ManyFriend<int> &>(ManyFriend<int> & c, ManyFriend<int> & d);
因为它是所有ManyFriend具体化的友元,所以能够访问所有具体化的item的成员,但它只访问了ManyFriend<int>对象。
同样,show2(hfd, hfi2)与下面具体化匹配:
void show2<ManyFriend<double> &, ManyFriend<int> &>(ManyFriend<double> & c, ManyFriend<int> & d);
它也是所有ManyFriend具体化的友元,并访问了ManyFriend<double>对象的item成员和ManyFriend<int>对象的item成员。
#include<iostream> using std::cout; using std::endl; template<typename T> class ManyFriend { private: T item; pulbic: ManyFriend(const T & i) : item(i) {} template<typename C, typename D> void show2(C & c, D & d); }; template <typename C, typename D> void show2(C & c, D & d) { cout << c.item << " , " << d.item << endl; } int main() { ManyFriend<int> hfi1(10); ManyFriend<int> hfi2(20); ManyFriend<double> hfdb(10.5); cout << "hfi1, hfi2 : "; show2(hfi1, hfi2); cout << "hfdb, hfi2 : "; show2(hfdb, hfi2); return 0; }
// output
hfi1, hfi2 : 10, 20
hfdb, hfi2 : 10.5, 20
其他内容如:递归使用模板,模板参数,类模板,成员模板,模板别名等,以后待续……
参考:《C++ Primer Plus(第六版)》、《Effective C++》……