类模板三种类模板参数
实际上有三种类型模板参数:类型模板参数、模板模板参数(以模板作为模板的参数)、无类型模板参数。
1、类型模板参数
类型模板参数是我们使用模板的主要目的。我们可以定义多个类型模板参数:
template
class Grid
{...}
同样,也可以为类型模板参数指定默认值:
#include
using std::vector;
template
class Grid
{...}
2、模板模板参数(template template parameter)
就是将一个模板作为另一个模板的参数。
正如上面的一个例子:
Grid
注意其中int出现了两次,必须指定Grid和vector的元素类型都是int。
如果写成:
Grid
因为vector本身就是一个模板,而不是一个类型,所以这就是一个模板模板参数。指定模板模板参数有点像在常规的函数中指定函数指针参数。
函数指针类型包括返回类型和函数的参数类型。在声明模板模板参数的时候也要包括完整的模板声明:
首先要知道作为参数的模板的原型,比如vector
template
class vector
{...};
然后就可以定义:
template
class Grid
{
public:
//Omitted for brevity
Container
};
模板模板参数的一般语法:
template
举例一个应用,Grid的一个构造函数:
template
Grid
mWidth(inWidth),mHeight(inHeight)
{
mCells=new Container
for(int i=0;i
}
使用的时候,与一般的没有什么区别:
Grid
myGrid.getElement(2,3);
注意:不要拘泥于它的语法实现,只要记住可以使用模板作为模板的一个参数。
3、无类型模板参数
无类型模板参数不能是对象,甚至不能是double或者float。无类型参数仅限于int、enmu、指针和引用。
有时可能想要允许用户指定一个特定值的元素来初始化空对象,可以使用以下的方法:
template
class Grid
{
public:
//Omitted for brevity
Grid(const Grid
Grid
//...
};
我们可以这样使用:
Grid
Grid
初始值可以是任意的int数,也就是必须是int、enmu、指针和引用的一种。
4、指针和引用模板参数
指针和引用模板参数必须指向所有翻译单元中都可用的全局变量。对于这些类型的变量,相应的技术术语就是带有外部连接的数据。
使用extern声明即可。
如:
template
class Grid
{...};
extern const int emptyInt=0;
Grid
对于初始化我们还可以使用“零初始化”即 T().
一、概念
利用模板特化机制实现编译期条件选择结构,利用递归模板实现编译期循环结构,模板元程序则由编译器在编译期解释执行。
模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型。
模板是一种对类型进行参数化的工具;
通常有两种形式:函数模板和类模板;
函数模板针对仅参数类型不同的函数;
类模板针对仅数据成员和成员函数类型不同的类。
使用模板的目的就是能够让程序员编写与类型无关的代码。比如编写了一个交换两个整型int 类型的swap函数,这个函数就只能实现int 型,对double,字符这些类型无法实现,要实现这些类型的交换就要重新编写另一个swap函数。使用模板的目的就是要让这程序的实现与类型无关,比如一个swap模板函数,即可以实现int 型,又可以实现double型的交换。模板可以应用于函数和类。下面分别介绍。
注意:模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。
二、通式
1、函数模板通式
template ...> 返回类型 函数名(参数列表)
{
... //函数体
}
其中template和class是关键字,class可以用typename 关键字代替,在这里typename 和class没区别。
<>括号中的参数叫模板形参,模板形参和函数形参很相像,模板形参不能为空。一但声明了模板函数就可以在该函数中使用内置类型的地方都可以使用模板形参名。模板形参需要调用该模板函数时提供的模板实参来初始化模板形参,一旦编译器确定了实际的模板实参类型就称他实例化了函数模板的一个实例。比如swap的模板函数形式为
template <class T> void swap(T& a, T& b){}
当调用这样的模板函数时类型T就会被被调用时的类型所代替,比如swap(a,b)其中a和b是int 型,这时模板函数swap中的形参T就会被int 所代替,模板函数就变为swap(int &a, int &b)。而当swap(c,d)其中c和d是double类型时,模板函数会被替换为swap(double &a, double &b),这样就实现了函数的实现与类型无关的代码。
注意:
对于函数模板而言不存在 h(int,int) 这样的调用,不能在函数调用的参数中指定模板形参的类型,对函数模板的调用应使用实参推演来进行,即只能进行 h(2,3) 这样的调用,或者int a, b; h(a,b)。
2、类模板通式
template<class 形参名,class 形参名,…> class 类名{
// 类定义... };
类模板和函数模板都是以template开始后接模板形参列表组成,模板形参不能为空,一但声明了类模板就可以用类模板的形参名声明类中的成员变量和成员函数,即可以在类中使用内置类型的地方都可以使用模板形参名来声明。比如
templateT> class A{public: T a; T b; T hy(T c, T &d);};
在类A中声明了两个类型为T的成员变量a和b,还声明了一个返回类型为T带两个参数类型为T的函数hy。
对于类模板,模板形参的类型必须在类名后的尖括号中明确指定。比如A<2> m;用这种方法把模板形参设置为int是错误的(编译错误:error C2079: 'a' uses undefined class 'A
,类模板形参不存在实参推演的问题。也就是说不能把整型值2推演为int 型传递给模板形参。要把类模板形参调置为int 型必须这样指定A
。
在类模板外部定义成员函数的方法为:
template<模板形参列表> 函数返回类型 类名<模板形参名>::函数名(参数列表){函数体}
比如有两个模板形参T1,T2的类A中含有一个void h()函数,则定义该函数的语法为:
template<class T1,class T2> void A<T1,T2>::h(){}
注意:当在类外面定义类的成员时template后面的模板形参应与要定义的类的模板形参一致。
再次提醒注意:模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。
其中,template 是声明类模板的关键字,表示声明一个模板,模板参数可以是一个,也可以是多个,可以是类型参数 ,也可以是非类型参数。类型参数由关键字 class或typename 及其后面的标识符构成。非类型参数由一个普通参数构成,代表模板定义中的一个常量。例:
template<class type,int width>
//type为类型参数,width为非类型参数 class Graphics;
注意:
(1)如果在全局域中声明了与模板参数同名的变量,则该变量被隐藏掉。
(2)模板参数名不能被当作类模板定义中类成员的名字。
(3)同一个模板参数名在模板参数表中只能出现一次。
(4)在不同的类模板或声明中,模板参数名可以被重复使用。
三、优劣及适用情况
通过将计算从运行期转移至编译期,在结果程序启动之前做尽可能多的工作,最终获得速度更快的程序。也就是说模板元编程的优势在于:
1.以编译耗时为代价换来卓越的运行期性能(一般用于为性能要求严格的数值计算换取更高的性能)。通常来说,一个有意义的程序的运行次数(或服役时间)总是远远超过编译次数(或编译时间)。
2.提供编译期类型计算,通常这才是模板元编程大放异彩的地方。
模板元编程技术并非都是优点:
1.代码可读性差,以类模板的方式描述算法也许有点抽象。
2.调试困难,元程序执行于编译期,没有用于单步跟踪元程序执行的调试器(用于设置断点、察看数据等)。程序员可做的只能是等待编译过程失败,然后人工破译编译器倾泻到屏幕上的错误信息。
3.编译时间长,通常带有模板元程序的程序生成的代码尺寸要比普通程序的大,
4.可移植性较差,对于模板元编程使用的高级模板特性,不同的编译器的支持度不同。
四、技术细节
模板元编程使用静态C++语言成分,编程风格类似于函数式编程,在模板元编程中,主要操作整型(包括布尔类型、字符类型、整数类型)常量和类型,不可以使用变量、赋值语句和迭代结构等。被操纵的实体也称为元数据(Metadata),所有元数据均可作为模板参数。
由于在模板元编程中不可以使用变量,我们只能使用typedef名字和整型常量。它们分别采用一个类型和整数值进行初始化,之后不能再赋予新的类型或数值。如果需要新的类型或数值,必须引入新的typedef名字或常量。
五、其他范例
// 主模板
template<int N>
struct Fib
{
enum { Result = Fib1>::Result + Fib2>::Result }; }; // 完全特化版 template <> struct Fib<1> { enum { Result = 1 }; }; // 完全特化版 template <> struct Fib<0> { enum { Result = 0 }; }; int main() { int i = Fib<10>::Result; // std::cout << i << std::endl; }
// 仅声明
struct Nil;
// 主模板
template <typename T> struct IsPointer { enum { Result = false }; typedef Nil ValueType; }; // 局部特化 template <typename T> struct IsPointer { enum { Result = true }; typedef T ValueType; }; // 示例 int main() { cout << IsPointer<int*>::Result << endl; cout << IsPointer<int>::Result << endl; IsPointer<int*>::ValueType i = 1; //IsPointer::ValueType j = 1; // 错误:使用未定义的类型Nil }
//主模板
template<bool>
struct StaticAssert;
// 完全特化 template<> struct StaticAssert<true> {}; // 辅助宏 #define STATIC_ASSERT(exp)\ { StaticAssert<((exp) != 0)> StaticAssertFailed; } int main() { STATIC_ASSERT(0>1); }
参考资料
[1] http://www.cnblogs.com/salomon/archive/2012/06/04/2534787.html
[2] http://www.cnblogs.com/assemble8086/archive/2011/10/02/2198308.html
[3] http://www.cnblogs.com/gw811/archive/2012/10/25/2738929.html
一、模板形参概述
有三种类型的模板形参:类型形参,非类型形参和模板形参。
二、类型形参
2.1 、类型模板形参
类型形参由关见字class或typename后接说明符构成,如template
其中T就是一个类型形参,类型形参的名字由用户自已确定。模板形参表示的是一个未知的类型。模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型说明符或类类型说明符的使用方式完全相同,即可以用于指定返回类型,变量声明等。
2.2、 不能为同一个模板类型形参指定两种不同的类型
比如:
template
,语句调用h(2, 3.2)将出错,因为该语句给同一模板形参T指定了两种类型,第一个实参2把模板形参T指定为int,而第二个实参3.2把模板形参指定为double,两种类型的形参不一致,会出错。(针对函数模板)
注意:上面的结论针对函数模板是正确的,但是不适用于类模板。下面将对类模板的情况进行补充。
当我们声明类对象为:A
,比如template
,语句调用a.g(2, 3.2)在编译时不会出错,但会有警告,因为在声明类对象的时候已经将T转换为int类型,而第二个实参3.2把模板形参指定为double,在运行时,会对3.2进行强制类型转换为3。当我们声明类的对象为:A
,此时就不会有上述的警告,因为从int到double是自动类型转换。
验证代码如下:
//TemplateDemo.h
#ifndef TEMPLATE_DEMO_HXX
#define TEMPLATE_DEMO_HXX
template<class T> class A{ public: T g(T a,T b); A(); }; #endif //TemplateDemo.cpp #include #include "TemplateDemo.h" template<class T> A::A(){} template<class T> T A::g(T a,T b){ return a+b; } void main(){ A<int> a; cout<2,3.2)<
编译结果:
warning C4244: “参数”: 从“double”转换到“int”,可能丢失数据
三、非类型形参
(1)模板的非类型形参
模板的非类型形参也就是内置类型形参,如template
其中int a就是非类型的模板形参。
(2)非类型形参在模板定义的内部是常量值,也就是说非类型形参在模板的内部是常量。
(3)模板的非类型形参只能是整型,指针和引用
像double,String, String **
这样的类型是不允许的。但是double &,double *
,对象的引用或指针是正确的。
(4)调用非类型模板形参的实参必须是一个常量表达式,即他必须能在编译时计算出结果。
(5)注意:任何局部对象,局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用作非类型模板形参的实参。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用作非类型模板形参的实参。
(6) 全局变量的地址或引用,全局对象的地址或引用const类型变量是常量表达式,可以用作非类型模板形参的实参。
(7)sizeof表达式的结果是一个常量表达式,也能用作非类型模板形参的实参。
(8)当模板的形参是整型时调用该模板时的实参必须是整型的,且在编译期间是常量,比如template
如果有int b,这时A
将出错,因为b不是常量,如果const int b
,这时A
就是正确的,因为这时b是常量。
(9)非类型形参一般不应用于函数模板中,比如有函数模板template
,若使用h(2)调用会出现无法为非类型形参a推演出参数的错误,对这种模板函数可以用显示模板实参来解决,如用h
这样就把非类型形参a设置为整数3。显示模板实参在后面介绍。
(10) 非类型模板形参的形参和实参间所允许的转换
a、允许从数组到指针,从函数到指针的转换。如:template
即数组到指针的转换
b、const修饰符的转换。如:template
即从int *到const int *
的转换。
c、提升转换。如:template
即从short到int 的提升转换
d、整值转换。如:template
即从int 到unsigned int的转换。
e、常规转换。
实例:
//由用户自己亲自指定栈的大小,并实现栈的相关操作
//TemplateDemo.h
#ifndef TEMPLATE_DEMO_HXX
#define TEMPLATE_DEMO_HXX
template<class T,int MAXSIZE> class Stack{//MAXSIZE由用户创建对象时自行设置 private: T elems[MAXSIZE]; // 包含元素的数组 int numElems; // 元素的当前总个数 public: Stack(); //构造函数 void push(T const&); //压入元素 void pop(); //弹出元素 T top() const; //返回栈顶元素 bool empty() const{ // 返回栈是否为空 return numElems == 0; } bool full() const{ // 返回栈是否已满 return numElems == MAXSIZE; } }; template <class T,int MAXSIZE> Stack::Stack():numElems(0){ // 初始时栈不含元素 // 不做任何事情 } template <class T,int MAXSIZE> void Stack::push(T const& elem){ if(numElems == MAXSIZE){ throw std::out_of_range("Stack<>::push(): stack is full"); } elems[numElems] = elem; // 附加元素 ++numElems; // 增加元素的个数 } template<class T,int MAXSIZE> void Stack::pop(){ if (numElems <= 0) { throw std::out_of_range("Stack<>::pop(): empty stack"); } --numElems; // 减少元素的个数 } template <class T,int MAXSIZE> T Stack::top()const{ if (numElems <= 0) { throw std::out_of_range("Stack<>::top(): empty stack"); } return elems[numElems-1]; // 返回最后一个元素 } #endif
//TemplateDemo.cpp
#include
#include
#include
#include #include "TemplateDemo.h" int main(){ try { Stack<int,20> int20Stack; // 可以存储20个int元素的栈 Stack<int,40> int40Stack; // 可以存储40个int元素的栈 Stack<std::string,40> stringStack; // 可存储40个string元素的栈 // 使用可存储20个int元素的栈 int20Stack.push(7); std::cout << int20Stack.top() << std::endl; //7 int20Stack.pop(); // 使用可存储40个string的栈 stringStack.push("hello"); std::cout << stringStack.top() << std::endl; //hello stringStack.pop(); stringStack.pop(); //Exception: Stack<>::pop<>: empty stack return 0; } catch (std::exception const& ex) { std::cerr << "Exception: " << ex.what() << std::endl; return EXIT_FAILURE; // 退出程序且有ERROR标记 } }
参考资料
[1] http://www.cnblogs.com/gw811/archive/2012/10/25/2738929.html
一、类模板的默认模板参数原则
1、可以为类模板的类型形参提供默认值,但不能为函数模板的类型形参提供默认值。函数模板和类模板都可以为模板的非类型形参提供默认值。
2、类模板的类型形参默认值形式为:
template<class T1, class T2=int> class A{};
为第二个模板类型形参T2提供int型的默认值。
3、类模板类型形参默认值和函数的默认参数一样,如果有多个类型形参则从第一个形参设定了默认值之后的所有模板形参都要设定默认值,比如
template<class T1=int, class T2>class A{};
就是错误的,因为T1给出了默认值,而T2没有设定。
4、在类模板的外部定义类中的成员时template 后的形参表应省略默认的形参类型。比如
template<class T1, class T2=int> class A{public: void h();};
定义方法为
template<class T1,class T2> void A<T1,T2>::h(){}
二、验证上述原则
//定义带默认类型形参的类模板。这里把T2默认设置为int型。
template<class T1,class T2=int> class CeilDemo{ public: int ceil(T1,T2); }; //在类模板的外部定义类中的成员时template 后的形参表应省略默认的形参类型。 template<class T1,class T2> int CeilDemo::ceil(T1 a,T2 b){ return a>>b; } int main(){ CeilDemo<int> cd; cout<ceil(8,2.5)<return 0; }
输出2(8右移2位),另外会报一个double转int会丢失信息的warning。
在类模板的外部定义类中的成员时template 后的形参表应省略默认的形参类型,如果没有省略,可能会依编译器不同有不同的处理方案(之前的vc可能只是报warning),我在vs2012和g++上是报错:
error C4519: 仅允许在类模板上使用默认模板参数
可见这里编译器将这里的默认参数认为是函数模板的。
template<class T1=int,class T2=double,class T3=double> class CeilDemo{ public: double ceil(T1,T2,T3); }; template<class T1,class T2,class T3> double CeilDemo::ceil(T1 a,T2 b,T3 c){ return a+b+c; } void main(){ CeilDemo<> cd; cout<ceil(2.5 ,3 ,4)<
输出9
三、测试案例汇总
//类模板非类型形参示例
//模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。
//类模板的定义
template<class T>class A{public:T g(T a, T b); A();}; //定义带有一个类模板类型形参T的类A template<class T1,class T2>class B{public:void g();}; //定义带有两个类模板类型形参T1,T2的类B //定义类模板的默认类型形参,默认类型形参不适合于函数模板。 template<class T1,class T2=int> class D{public: voidg();}; //定义带默认类型形参的类模板。这里把T2默认设置为int型。 //templateclass E{}; //错误,为T1设了默认类型形参则T1后面的所有形参都必须设置认默值。 //以下为非类型形参的定义 //非类型形参只能是整型,指针和引用,像double,String, String **这样的类型是不允许的。但是double &,double *对象的引用或指 针是正确的。 template<class T1,int a> class Ci{public:void g();}; //定义模板的非类型形参,形参为整型 template<class T1,int &a>class Cip{public:void g();}; template<class T1,A<int>* m> class Cc{public:void g();}; //定义模板的模板类型形参,形参为int型的类A的对象的指针。 template<class T1,double*a>class Cd{public:void g();}; //定义模板的非类型形参,形参为double类型的引用。 class E{}; template<class T1,E &m> class Ce{}; //非类型模板形参为对象的引用。 //以下非类型形参的声明是错误的。 //templateclass Cc{}; //错误,对象不能做为非类型形参,非类型模板形参的类型只能是对象的引用或指针。 //templateclass Cc{}; //错误,非类型模板的形参不能是double类型,可以是double的引用。 //template m>class Cc{}; //错误,非类型模板的形参不能是对象,必须是对象的引用或指针。这条规则对于模板型参 也不例外。 //在类模板外部定义各种类成员的方法, //typeid(变量名).name()的作用是提取变量名的类型,如int a,则cout<template<class T> A::A(){cout<<"class A goucao"<<typeid(T).name()<//在类模板外部定义类的构造函数的方法 template<class T> T A::g(T a,T b){cout<<"class A g(T a,T b)"<//在类模板外部定义类模板的成员 template<class T1,class T2> voidB::g(){cout<<"class g f()"<<typeid(T1).name()<<typeid(T2).name()<//在类外面定义类的成员时template后面的模板形参应与要定义的类的模板形参一致 template<class T1,int a> voidCi::g(){cout<<"class Ci g()"<<typeid(T1).name()<template<class T1,int &a> voidCip::g(){cout<<"class Cip g()"<<typeid(T1).name()<//在类外部定义类的成员时,template后的模板形参应与要定义的类的模板形参一致 template<class T1,A<int> *m> voidCc::g(){cout<<"class Cc g()"<<typeid(T1).name()<template<class T1,double* a> voidCd::g(){cout<<"class Cd g()"<<typeid(T1).name()<//带有默认类型形参的模板类,在类的外部定义成员的方法。 //在类外部定义类的成员时,template的形参表中默认值应省略 template<class T1,class T2> voidD::g(){cout<<"class D g()"<//template void D::g(){cout<<"class D k()"<template的形参表中默认值应省略。 //定义一些全局变量。 int e=2; doubleed=2.2; double*pe=&ed; A<int> mw; A<int> *pec=&mw; E me; //main函数开始 int main() { // templatevoid h(){} //错误,模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行。 //A<2> m; //错误,对类模板不存在实参推演问题,类模板必须在尖括号中明确指出其类型。 //类模板调用实例 A<int> ma; //输出"class A goucao int"创建int型的类模板A的对象ma。 B<int,int> mb; mb.g(); //输出"class B g() int int"创建类模板B的对象mb,并把类型形参T1和T2设计为int //非类型形参的调用 //调用非类型模板形参的实参必须是一个常量表达式,即他必须能在编译时计算出结果。任何局部对象,局部变量,局部对象的地址,局部 变量的地址都不是一个常量表达式,都不能用作非类型模板形参的实参。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能 用作非类型模板形参的实参。 //全局变量的地址或引用,全局对象的地址或引用const类型变量是常量表达式,可以用作非类型模板形参的实参。 //调用整型int型非类型形参的方法为名为Ci,声明形式为template class Ci Ci<int,3>//正确,数值R是一个int型常量,输出"class Ci g() int" const int a2=3; Ci<int,a2> mci1; mci1.g(); //正确,因为a2在这里是const型的常量。输出"class Ci g() int" //Ci mci; //错误,int型变量a是局部变量,不是一个常量表达式。 //Ci mci; //错误,全局int型变量e也不是一个常量表达式。 //调用int&型非类型形参的方法类名为Cip,声明形式为templateclass Cip Cip<int,e> mcip; //正确,对全局变量的引用或地址是常量表达式。 //Cip mcip1; //错误,局部变量的引用或地址不是常量表达式。 //调用double*类型的非类形形参类名为Cd,声明形式为templateclass Cd Cd<int,&ed> mcd; //正确,全局变量的引用或地址是常量表达式。 //Cd mcd1; //错误,全局变量指针不是常量表达式。 //double dd=3.3; //错误,局部变量的地址不是常量表达式,不能用作非类型形参的实参 //Cd mcd; //错误,非类型形参虽允许一些转换,但这个转换不能实现。 //调用模板类型形参对象A *的方法类名为Cc,声名形式为template* m> class Cc Cc<int,&mw> mcc; mcc.g(); //正确,全局对象的地址或者引用是常量表达式 //Cc mcc; //错误,局部变量的地址或引用不是常量表达式。 //Cc mcc2; //错误,全局对象的指针不是常量表达式。 //调用非类型形参E&对象的引用的方法类名为Ce。声明形式为template class Ce E me1; //Ce mce1; //错误,局部对象不是常量表达式 Ce<int,me> mce; //正确,全局对象的指针或引用是常量表达式。 //非类型形参的转换示例,类名为Ci //非类型形参允许从数组到指针,从函数到指针的转换,const修饰符的转换,提升转换,整值转换,常规转换。 const short s=3; Ci<int,s> mci4†//正确,虽然short型和int不完全匹配,但这里可以将short型转换为int型
参考资料
[1] http://www.cnblogs.com/gw811/archive/2012/10/25/2736224.html(注:此文有多处问题,请抱着谨慎态度查看)