模板提供了对泛型编程的直接支持,也就是说,使用类型作为参数的程序设计。C++模板机制就是在定义类和函数时使用类型作为参数,比如templateclass a{}; 或者templatefun().
A template depends only on the properties that it actually uses from its parameter types and does not require different types used as arguments to be explicitly related。这句话的意思就是一个模板的特性依赖于其真正的参数的特性,并且不同参数之间不需要存在显示的关联。比方说一个模板的参数类型不需要来自同一个继承层次结构。
看个例子:
Template class String{
Struct Srep;
Srep *rep;
Public:
String();
String(const C*);
String(const String&);
C read(int i) const;
};
Template是一个前缀,表示后面定义的是一个模板,class C是模板参数,这里C是一个类型的名字,而不仅仅只是一个类的名字。
上面定义了一个类模板,要生成一个类或者说一个模板类,就需要传入参数(注意类模板和模板类的区别):
String cs;
String us;
String ws;
这里生成了3个模板类,并且分别定义/声明(这里不打算区分这两个概念了,因为不是关键)了3个相应的类实例。
String是类模板的名字,后面跟上<>就表示了一个模板类的名字,char,unsigned char,wchar_t是模板参数,这样string,string,string就表示了3个类,除了这种比较奇怪的表示方式外,跟一般的类的使用是没有两样的,实际上总是可以写出对应的“手写”的类定义的:
Class String{
Struct Srep;
Srep *rep;
Public:
String();
String(const char);
String(const String&);
Char read(int i) const;
};
这个 String类跟String在语义上是完全等同的。类似的也可以写出与String,String对应的语义等同的“手写”版本的类。因此在实现一个类模板之前,总是可以先用一个手写的类做一些测试,然后再加以通用化。
至于cs,us,ws这是3个变量,或者说是3个类实例。
另外可以使用typedef来缩短模板类的名字长度,并且在某种程度上可以隐藏细节,比方说标准库的string类:
Typedef basic_string string;
模板类的成员与非模板类的成员在语义上也是一样的,也就是说模板类的成员的声明与定义没有什么特殊的地方。
一个模板成员(成员带有模板)不必一定在模板类内部定义(注意定义跟声明的区别,这是基本概念),但是必定要在某个地方给出定义,这跟非模板成员是一致的。
Members of a template class are themselves templates parameterized by the parameters of their template class,也就是说,模板成员的参数就是模板类本身的参数。因此,如果要在类外部定义一个模板成员,就必须明确的以模板形式声明:
1.Templatestruct String::Srep{
C* s;
Int sz;
Int n;
//...
};
2.TemplateC String::read(int i) const{return rep->s(i);}
3.TemplateString::String()
{
Rep=new Srep(0,C());
}
这里有3个定义:
template是前缀,跟定义类模板时的语义是一样的。
1.Struct 表示这是一个struct的定义,String::Srep表示Srep是String的一个成员;
2.C 是返回值,具体是啥由模板参数确定
3.在String的作用范围内,用来修饰对模板本身是冗余的,因此String::String()是个构造函数,如果你愿意也可以写成String::String().
类模板的名字是不能重复定义的,比方说:
template class String{//...};
Class String{//...};
编译时会提示重复定义的错误。
A type used as a template argument must provide the interface expected by the template.就是说一个模板参数必须提供模板所期望的操作,比方说上面String类的参数类型就必须提供拷贝操作(拷贝构造函数,拷贝赋值运算符)。
通过一个模板类(String<>)和模板参数(char)产生出一个类的声明称为模板instantiation(实例化),类似的还有函数instantiation。由某模板参数决定的一个特定的模板称为specialization。可见specialization是instantiation的结果。这个过程是由编译器来实现的。
A template can take type parameters,parameters of ordinary types such as ints,and template,并且可以带多个参数:
Templateclass Cont{/*...*/};
Type parameters指的是用类型的名字做参数,parameters of ordinary types指的是用属于某个类型的值做参数,最后是用模板做模板的参数。
整型的参数一般用来提供大小或者某些限制:
Templateclass Buffer{
T v[i];
Int sz;
Public:
Buffer():sz(i){}
//...
};
Buffer cbuf;
Buffer rbuf;
在强调下,Buffer和Buffer分别是两个类,cbuf和rbuf分别是这两个类的实例。
显然Buffer语义上等同于
class Buffer{
Char v[127];
Int sz;
Public:
Buffer():sz(127){}
//...
};
类似的也可以写出与Buffer语义相同的“手写”类。我们可以认为编译器生成的模板类跟上面这个手写类是一样的。
一个模板参数可以是一个常量表达式,也就是说这个参数必须能够在编译阶段确定其值:
Void f(int i)
{
Buffer(int,i); //i在编译阶段是不知道什么值的,因此不能作为模板参数
}
要确定一点,模板是个静态的概念,也就是说模板在编译时而不是在运行时实例化的。
模板的参数可以是the address of an object or functions with external linkage,也可以是一个non-overloaded pointer to member。
指针作为模板参数必须使用这样的形式:&of,of是一个对象或者函数的名字,当然如果of是个函数的话,也可以用of。如果这个指针指向成员,则必须是这样的形式:&X::of.
另外,字符串文本作为模板是不可接受的,因此不能在模板中使用参数来初始化一个字符串(这是我目前想得到的可能以字符串文本做模板参数的用法)。原因么大概是这样,指针必须是以&of的形式出现,而一个字符串文本在作为参数传递的时候会退化成指针,这个指针没有名字,并且不会以&of的形式出现,所以字符串文本无法作为模板参数(很偷懒的办法)。
看下这几个例子:
String s1;
Typedef unsigned char uchar;
String s2;
这两个类是一样的类型。
Buffer与Buffer也是一样的类型。
编译器会对模板定义做句法和一些语义上的常规检查,常规的意思就是对普通类也适用,不过不是所有的错误都可以在这个时候检查出来,有些错误时要在实例化时才发现,这类错误自然是模板特有的错误,比方说一个类没有定义<<运算符,但是模板当中使用了这个运算符,这个错误只有在用这个类实例化模板的时候才能发现。自然也有些错误只有到了运行时才会被发现,这类错误一般都是逻辑错误了。
关于模板函数,先看例子:
Templatevoid sort(vector&);
跟以前一样Template表示模板前缀,void是函数返回值,sort是函数名字,vector&是个引用参数,这个参数是一个T的vector。
接下来再看:
Void f(vector& vi,vector& vs)
{
Sort(vi);
Sort(vs);
}
从这里我们可以看到,当调用一个模板函数时,函数的参数决定了使用哪一个版本的模板,也就是说,模板参数是通过函数参数来推断的。
前面只是sort函数的声明,跟普通函数一样,模板函数也需要在某个地方进行定义:
Templatevoid sort(vector& v)
//这里使用了shell排序,当然也可以有其他的排序算法
{
Const size_t n=v.size();
For(int gap=n/2;gap>0;gap/=2)
For(int i=gap;i
For(int j=i-gap;j>=0;j-=gap)
If(v[j+gap])
T temp=v[j];
V[j]=v[j+gap];
V[j+gap]=temp;
}
}
当然这里要注意的是T类型必须定义了‘<’运算。
再看这个例子:
TemplateT& lookup(Buffer& b,const char *p);
Class Record{
Const char[12];
//...
};
Record& f(Buffer& buf, const char *p)
{
Return lookup(buf,p);
}
这里,通过lookup的参数buf推断出T是Record,i是128。
这种从函数参数推断出函数模板参数的能力是至关重要的。当然这个推断过程是编译器的事,作为一个程序员,需要知道或者说需要做的是对有可能作为模板参数的类型定义必要的操作,并且了解了一种创建对象的方法。
注意,类模板参数是不做这种推断的,原因就在于:the flexibility provided by several constructors for a class would make such a deduction impossible in many cases and obscure in many more。为什么这么说呢?首先要搞清楚这里的推断指的是通过函数的参数,通常是一个实例,来判断出模板参数从而决定使用哪一个模板函数。例如f这个函数,传入的buf参数是一个已经实例化的模板类,在调用lookup时,通过buf函数推断出T是Record,i是128,因此调用的模板函数就是lookup(Buffer& b,const char *),当然,这个模板函数必然是在某个地方有定义的。
我们再看类的情况,一个类可能拥有多个不同参数的构造函数,对一个没有带模板的构造函数如何推断模板参数?很显然这种做法很奇怪也不大可能。虽然似乎可以对带模板参数的构造函数进行某些推断,但是这样就会出现对构造函数处理的不一致,于是一刀切干脆不做。
如果不能推断出模板参数(函数模板的参数),那么就必须明确的指定:
Templateclass vector{/*...*/};
TemplateT* create();
Void f()
{
//实例化一个模板类vector,并创建一个实例v,这里只是做个比较,并非说明
Vector v;
//函数的情况
Int *p=create();
}
我们可以看到,create这个函数没有参数,因此无法推断出模板参数来,这时候就需要通过create()的形式来显式的指出T是int,使用的模板函数版本是int * create();
顺便说下template只是个前缀,不存在template int* create()这样的句法,同样也不存在template class vector{};这样的句法,而分别应该是create()和vector。就是说模板前缀只在声明或者定义的时候存在,实例化的时候是不使用的。不过对于模板类像template<>class C{};这样的句法是存在的,后面会有解释。
这种方式(显式的给出模板参数)经常用于给定一个模板函数的返回值:
TemplateT implicit_cast(U u){return u;}
Void g(int i)
{
Implicit_cast(i); //错误,无从推断T是啥
Implicit_cast(i); //T是double,U是int
Implicit_cast(i); //T是char,U是double,显然i会做一次转换
Implicit_cast(i);
//T是char*,U是int,不过由于不存在从int到char*的转换,这里会出错
}
从上面我们还可以看到,模板参数可以省略,跟缺省函数参数机制是一样的,也就是只有末尾的参数可以省略。
看例子:
TemplateT max(T,T);
Const int s=7;
Void k()
{
Max(1,2); //max(1,2);
Max('a','b'); //max('a','b');
Max(2.7,4.9); //max(2.7,4.9)
Max(s,7); //max(int(s),7) s是个const int,所以做了个转换
Max('a',1); //错误:ambiguous ,显然不存在从char到int的提升,也没有标准的转换
Max(2.7,4); //错误:同上
}
不过对于上面的最后两个错误,可以显式加以限定:
Void f()
{
Max('a',1); //max(int('a'),1);
Max(2.7,4); //max(2.7,double(4)),如果是max()会如何?
}
也可以增加一些适当的声明:
Inline int max(int i,int j){return max(i,j);}
Inline double max(int i,double d){return max(i,d);}
Inline double max(double d,int i){return max(d,i);}
Inline double max(double d1,double d2){return max(d1,d2);}
下面是跟继承结合起来的例子:
Templateclass B{/*...*/};
Templateclass D:public B{/*...*/};
Templatevoid f(B* pd);
Void g(B* pb,D * pd)
{
f(pb); //f(pb);
f(pd); //f(static_cast*>(pd));这里做了一个标准转换
}
对于没有参与模板参数推断的函数参数的语义与一个非模板函数的参数等同,也就意味着会自动做类型转换。当然这并不意味着参与推断的参数就不做类型转换,不做转换的是模板参数,而不是函数参数,如上面的例子所示,int作为模板参数不存在转换的问题,pd由一个D转换成B*,值得注意的是两个模板的模板参数都是int。
函数模板可以使用缺省的模板参数:
Template>
Int compare(const String& str1,const String& str2)
{
For(int i=0;i
If(!C::eq(str1[i],str2[i]) return C::lt(str1[i],str2[i]?-1:1;
}
Return str1.length()-str2.length();
}
当然也可以用函数重载来实现:
Template
Int compare(const String& str1,const String& str2);//使用C来比较
Template
Int compare(const String& str1,const String& str2);//使用Cmp类来比较
因此:
Void f(String swede1,String swede2)
{
Compare(swede1,swede2);
Compare(swede1,swede2);
}
至于Cmp,Literate这些类,可参看原书$13.4,就不多说了。使用缺省模板参数可以提供一种比较简洁的实现。
我们再看user-defined specializations,或者user specializations。
考虑一个Vector模板:
Templateclass{
T* v;
Int sz;
Publice:
Vector();
Explicit Vector(int i); //避免出现某种很奇怪的隐含转换
T& elem(int i){return v[i];}
T& operator[ ](int i);
Void swap(Vector&);
};
Vector vi;
Vector vps;
Vector vs;
Vector vpc;
Vector vpn;
我们看到这里大部分specialization都使用了指针做参数,这是现实存在的现象,包括大多数stl容器。
假如我们定义了用Vector做参数的函数模板,并且在程序中使用不同的模板参数(T)多次调用这个函数,对于每一个调用编译器都要为这个函数生成一个不同的版本的specialization,于是很容易就会出现所谓的代码膨胀。
这个问题是很容易解决的,那就是指针容器共享一个implementation:
首先定义一个void*的Vector的specialization:
Template<>class Vector{
Void **p;
//...
Void*& operator[ ](int i);
};
Template<>前缀说明这是一个可以不用指定模板参数的specialization,而在这个specialization中用到的模板参数可以放在类名字后面的<>中,这在前面提到过的。具体到这个例子,说明这个定义可以作为所有T 是void* 的Vector的implementation。
Vector是个完整的specialization,因此使用的时候无需再对模板参数做指定或者推断,从而可以有这样声明/定义:
Vector vpv;
接下来需要一个所谓的partial specialization:
Template class Vector:private Vector{
Public:
Typedef Vector*> Base;
Vector():Base(){} //调用基类的构造函数,没有自己独有的操作
Explicit Vector(int i):Base(i){};
T*& elem(int i){return static_cast(Base::elem(i));}
T*& operator[ ](int i){return static_cast(Base::operator[ ](i));}
};
到这里我们很容易理解类名字后面的即所谓的specialization pattern使得这一个specialization可以用于所有的指针类型:
Vectorvps; //是,所以T是shape
Vectorvppi; //是,所以T是int*
也就是说当使用partial specialization时,模板参数是通过specialization pattern来推断的。
回头看template<>class vector。首先如前所述这是一个完整的specialization,即可以有Vector vpv这样的声明;其次这确定了指定的模板参数必然是void *。
再看Template class Vector:private Vector。这是一个所谓的partial specialization,顾名思义,部分而不是完全,因此必须指定模板参数才能构成完全的specialization。而对于Vector,比较上面例子的specialization与最初的Vector的specialization,可以看到两者虽然句法是一样的但语义已经不同了。
实际上虽然template<>vector{};是个specialization,同时也是一个类定义,并没有实例化一个对象,因此这样的句法是合法的,但是在实例化这个类的一个对象是还是只能用vector vpv这样的形式,当然template<>vector{/*...*/} vpv;这样的句法也是合法的,不过对此应该不会误解就不废话了。
需要再次(如果曾经)强调模板必须在specialization之前声明,具体到上面,必须是这样的顺序:
Templateclass Vector{/*...*/};
Template<>class Vector{/*...*/};
Template class Vector:private Vector{/*...*/};
不过如果vector不是vector的子类而是一个独立的类,顺序必须是这样:
Templateclass Vector{/*...*/};
Template class Vector{/*...*/};
Template<>class Vector{/*...*/};
也就是越general的就应该出现在越前面。
还有一个问题就是如下这样的情况:
Templateclass List{/*...*/};
List li;
Templateclass List{/*...*/};
这样是错误的,List li不是List而是List的specialization。当然如果没有List的声明或者把List 放到List后面出现就没有错了。这是个作用域的问题。
为什么这里List不能是List的specialization呢?一个partial specialization的声明限定了对于某种类型的specialization的模板参数必须是某种指定的类型,如这里的T*与对应的int*。想象一下,编译器看到List的定义,接着看到List li,于是指定了List的一个版本的specialization,然后又看到了List,于是又回头对li重新做指定,但是li出现的时候List尚未定义,于是。。。好吧,这里是我自己的想象,既然bs这么说了,我只好这么理解,别问为什么。
看看函数的例子:
Template
bool less(T a,T,b){ return a
显然如果T是一个指针(比方说数组或者C类型字符串的情况),这个函数的返回大多数情况下不是所期望的,这时候可以使用这样一个specialization:
Template<>bool less(const char* a,const char* b)
{
Return strcmp(a,b)<0;
}
跟类的情况一样,template<>表明这是一个无需模板参数加以指定的specialization,后面的表示这个specialization是当模板参数为const char*是所用的specialization。
自然,这个specialization可以写成这样:
Template<>bool less<>(const char* a,const char* b)
{
Return strcmp(a,b)<0;
}
并且还可以这样:
Template<>bool less(const char* a,const char* b)
{
Return strcmp(a,b)<0;
}
看起来跟函数重载类似,当然这里用函数重载来实现对char*的比较也是可以的。
总结一下吧,上面这些例子说明了在碰到一些特殊的参数类型时,我们可以使用user specialization来解决这些参数的specialization问题。当然这只是一种解决问题的方案,或者说这是c++提供的一种机制,所以不要问为什么要这样做,而是应该告诉自己原来可以这样做。
模板类也是可以继承的,其基类可以是一个非模板类(例子上面有),也可以是另一个模板类:
Templateclass Vector{};
Templateclass Vec: public Vector{};
通常子类和基类用同样的模板参数,不过这不是必须的,并且还可以有这样的情况:
Templateclass basic_ops{};
Templateclass math_container:public basic_ops>{};
一个类或者一个类模板的成员也可以有模板,不过作为成员的模板不能是虚的。
另外,由一个类模板产生出来的类之间没有缺省的任何关系,比方说有这么两个类:
Class shape{};
Class circle:public shape{};
显然这里有层次关系,但是对于set和set(我们假定templateclass set{};在某处有定义),没有任何理由认为这两个类之间也有层次关系。
end.
阅读(2524) | 评论(0) | 转发(0) |