函数模板
一.初探函数模板
函数模板的声明形式:
template< comma-separated-list-of-parameters >//template< 用逗号隔开的参数列表>
可以用class来替代typename,聪语义上讲,二者等价。因此,即使在这里使用class,你也可以用任何类型(前提是该类型提供模板使用的操作)来实例化模板参数。另外还应该注意,这种用法和类型声明不同,也就是说,在声明(引入)类型参数的时候,不能用关键字struct代替typename。
只要使用函数模板,(编译器)会自动地引发这样一个实例化过程,因此程序员并不需要额外地请求模板的实例化。
注:模板被编译了两次,分别发生在
1.实例化之前,先检查模板代码本身,查看语法是否正确;在这里会发现错误的语法;
2.在实例化期间,检查模板代码,查看是否所有的调用都有效。在这里会发现无效的调用,如该实例化类型不支持某些函数调用等。
二.实参的演绎
模板参数可以由我们所传递的实参来决定。
有三种方法来处理类型不匹配的错误:
1.对实参进行强制类型转换,使它们可以互相匹配:
max( static_cast<double>(4),4.2)
2.显示指定(或者限定)T的类型:
max<double>(4,4.2)
3.指定两个参数可以具有不同的类型。
三.模板参数
函数模板有两种类型的参数:
1.模板参数:位于函数模板名称的前面,在一对尖括号内部进行声明:
template<typename T> //T是模板参数
2.调用参数:位于函数模板名称之后,在一对圆括号内部进行声明:
…max(T const& a,T const& b)//a和b都是调用参数
因为调用参数的类型构造自模板参数,所以模板参数和调用参数通常是相关的。我们把这个概念称为:函数模板的实参演译。它让你可以像调用普通函数那样调用函数模板。
注:1.当模板参数和调用参数没有发生关联,或者不能由调用参数来决定模板参数的时候,你在调用时就必须显式指定模板实参。必须显示的指定模板实参列表。
四.重载函数模板
当一个非模板函数和一个同名的函数模板同时存在,且该函数模板还可以被实例化为这个非模板函数。对于非模板函数和同名函数模板,如果其他条件都是相同的话,在调用时,重载解析过程通常会调用非模板函数,而不会从该模板产生出一个实例。模板是不允许自动类型转化的;但普通函数类型可以进行自动类型转换。
注:在所有重载的实现里面,我们都是通过引用来传递每个实参的。
五.小结
1.模板函数为不同的模板实参定义了一个函数家族;
2.当你传递模板实参的时候,可以根据实参的类型来对函数模板进行实例化;
3.可以显式指定模板参数;
4.可以重载函数模板;
5.当重载函数模板的时候,其改变限制在:显式的指定模板参数;
6.一定要让函数模板的所有重载版本的声明都位于他们被调用的位置之前。
类模板
与函数相似,类也可以被一种或多种类型参数化。
一.类模板Stack的实现
类模板Stack<>是通过C++标准库的类模板vector<>来实现的;因此不需要自己实现内存管理、拷贝构造函数和赋值运算符;可以将精力放在该类模板的接口实现上。
类模板的声明和函数模板的生命很相似;在声明之前,先(用一条语句)声明作为类型参数的标识符。
为定义类模板的成员函数,需指定该成员函数是一个函数模板,且还需要使用这个类模板的完整类型限定符。(Stack<T>::)。
对于类模板的任何成员函数,你都可以把它实现为内联函数,将它定义于类声明里面。
注:需要在两个靠在一起的模板尖括号(即>)之间留一个空格;否则,编译器将会认为你是在使用operator>>,而这将会导致一个语法错误:
Stack<Stack<int>> intStackStack; // ERROR:这里不允许使用>>
二.类模板的特化
可以用模板实参来特化类模板。通过特化类模板,可以优化及于某种特定类型的实现,或者克服某种特定类型在实例化类模板时所出现的不足。如果要特化一个类模板,你还要特化该类模板的所有成员函数。虽然也可以只特化某个成员函数,但这个做法并没有特化整个类,也就没有特化整个类模板。
为了特化一个类模板,你必须在起始处声明一个template<>,接下来声明用来特化类模板的类型。这个类型被用作模板实参,且必须在类名的后面直接指定:
template<>
class Stack<std::string>{
……
}
进行类模板的特化时,每个成员函数都必须重新定义为普通函数,原来模板函数中的每个T也相应地被进行特化的类型取代:
void Stack<std::string>::push(std::string const& elem)
{
elems.push_back(elem);//附加传入实参elem的拷贝
}
注:deque会释放内存;当需重新分配内存时,deque的元素并不需要被移动。
三.局部特化
如果有多个局部特化同等程度的匹配某个声明,那么就称该声明具有二义性。
四.缺省模板实参
对于类模板,你还可以为模板参数定义缺省值;这些值就被称为缺省模板实参;且它们可以引用之前的模板参数。
在类Stack<>中,可以把用于管理元素的容器定义为第二个模板参数,并且使用std::vector<>作为它的缺省值;当在程序中声明Stack对象的时候,你还可以指定容器的类型。
五.小结
1.类模板是具有如下性质的类:在类的实现中,可以有一个或者多个类型还没有被指定;
2.为了使用类模板,你可以传入某个具体类型作为模板实参;然后编译器将会基于该类型来实例化类模板;
3.对于类模板而言,只有那些被调用的成员函数才会被实例化;
4.可以用某种特定类型特化类模板;
5.可以用某种特定类型局部特化类模板;
6.可以为类模板的参数定义缺省值,这些值还可以引用之前的模板参数。
非类型模板参数
非类型模板参数是有限制,通常而言,它们可以是常函数(包括枚举值)或者指向外部链接对象的指针。
注:1.由于字符串文字是内部链接对象(因为两个具有相同名称但处于不同模块的字符串,是两个完全不同的对象),所以你不能使用它们来作为模板实参;
2.不能使用全局指针作为函数模板;
小结:1.模板可以具有值模板参数,而不仅仅是类型模板参数;
2.对于非类型模板参数,你不能使用浮点数、class类型的对象和内部链接对象(例如string)作为实参。
模板的技巧性基础知识
一.关键字typename
在C++标准化的过程中,引入关键字typename是为了说明:末班内部的标识符可以是一个类型。
譬如下面的例子:
template<typename T>
class MyClass{
typename T::SubType *ptr;
……
};
上例中,第2个typename被用作说明:SubType是一个定义于类T内部的一个类型。因此,ptr是一个指向T::SubType类型的指针。
若不使用typename,SubType就会被认为是一个静态成员,那么他应该是一个具体的变量或对象,于是,如下表达式:
T::SubType * ptr
会被看做是类T的静态成员SubType和ptr的乘积。
注:1.通常而言,当某个依赖于模板参数的名称是一个类型是,就应该使用typename。
2. .template构造
考虑下面这个使用标准bitset类型的例子
template<int N>
void printBitset(std::bitset<N> const &bs)
{
std::cout<<bs.template to_string<char,char_traits<char>,allocator<char> >();
}
上例中.template说明:bs.template后面的小于号(<)是模板实参列表的起始符号;只有在编辑其判断小于号之前呢,存在依赖于模板参数的构造,才会出现这种问题。在本例中,传入参数bs就是依赖于模板参数N的构造。
{只有该前面存在依赖于模板参数的对象时,我们才需要在模板内部使用.template标记(和类似的诸如->template的标记),而且这些标记也只能在模板中才能作用。}
二.使用this->
对于具有基类的类模板,自身使用名称X并不等同于this->x。即使该x是从基类继承获得的,也是如此。例如:
template <typename T>
class Base{
public:
void exit();
};
template <typename T>
class Derived : Base<T>{
public:
void foo();
exit();
}
};
在上例中,在foo()内部决定要调用哪个exit()时,并不会考虑基类Base中定义的exit()。因此,你如果不是获得错误,就是调用了另一个exit()。
注:对于那些在基类中的声明,并且依赖于模板参数的符号(函数或者变量的符号),应在它们前面使用this->或者Base<T>::。如果希望避免不确定性,你可以(使用诸如this->和Base<T>::等)限定(模板中)所有的成员访问。
三.成员模板
类成员也可以是模板。嵌套类和成员函数都可以作为模板。
(缺省赋值运算符要求两边具有相同的类型。)
定义成员模板的语法:在定义有模板参数T的模板内部,还定义了一个含有模板参数T2的内部模板:
template <typename T>
template <typename T2>
注:1.对于类模板而言,只有那些被调用的成员函数才会实例化。因此,如果元素类型不同的类模板之间没有进行相互赋值,就可以使用vector来作为内部容器。
2.因为自定义的模板赋值运算符并不是必不可少的,所以不存在push_front()的情况下,某些程序并不会出现错误信息,而且能正确运行。
四.模板的模板参数(VC6不支持,VC7支持)
用stack为例,如果要使用一个缺省值不同的内部容器,程序员必须两次指定元素类型。也就是说,为了指定内部容器的类型,需要同时传递容器的类型和它所含元素的类型。如下:
Stack<int,std::vector<int> >vStack;
然而借助模板的模板参数,你可以只指定容器的类型而不需要制定所含元素的类型,就可以声明这个Stack类模板。
Stack<int,std::vector>vStack;
为获得此特性,必须把第2个模板参数指定位模板的模板参数。在使用时,第2个参数必须是一个类模板,并且由第一个模板参数传递来的类型进行实例化。
注:1.之前说过作为模板参数的声明,通常可以使用typename来替换关键字class。然而,若是为了定义一个类,则只能使用关键字class。
2.函数模板并不支持模板的模板参数。
五.零初始化
对于int、double或者指针等基本类型,并不存在“用一个有用的缺省值来对它们进行初始化”的缺省构造函数;相反,任何未被初始化的局部变量都具有一个不确定值。
故我们应该显示的调用内建类型的缺省构造函数,并将缺省值设为0。如下:
template<typename T>
void foo()
{
T x = T();//如果T是内建类型,x是零或者false
}
对于类模板,在用某种类型实例化该模板,为了确认它所有的成员都已经初始化完毕,需要定义一个缺省构造函数,通过一个初始化列表来初始化类模板的成员:
template<typename T>
class MyClass{
private:
T x;
public:
MyClass():x(){//确认x已被初始化,内建类型也是如此
}
……
};
六.使用字符串作为函数模板的实参
对于非引用类型的参数,在实参演绎的过程中,会出现数组到指针的类型转换(这种转换通常也被称为decay)。
对于字符数组和字符串指针之间不匹配的问题并没有什么通用的解决方法。根据不同的情况,可以:
1.使用非引用参数,取代引用参数(然而,这可能会导致无用的拷贝)。
2.进行重载,编写接受引用参数和非引用参数的两个重载函数(然而,这可能会导致二义性)。
3.对具体类型进行重载(譬如对std::string进行重载)。
4.重载数组类型,譬如:
template <typename T,int N,int M>
T const* max(T const (&a)[N],T const (&b)[M])
{
return a<b ? b:a;
}
5.强制要求应用程序程序员使用显示类型转换。
七.小结
1.如果要访问依赖于模板参数的类型名称,应该在类型名称前添加关键字typename。
2.嵌套类和成员函数也可以是模板。
3.赋值运算符的模板版本并没有取代缺省赋值的运算符。
4.类模板也可以作为模板参数。我们称之为模板的模板参数。
5.模板的模板参数必须精确的匹配。匹配时并不考虑“模板的模板实参”的缺省模板实参(如std::deque的allocator)。
6.通过先是调用缺省构造函数,可以确保模板的变量和成员都已经用一个缺省值完成初始化,这种方法对内建类型的变量和成员也适用。
7.对于字符串,在实参演绎过程中,当且仅当参数不是引用时,才会出现数组到指针的类型转换。
模板实战
一.包含模型
对于非模板代码大多C和C++程序员会这样组织:
1.类(class)和其他类型都放在头文件中。
2.对于全局变量和(非内联)函数,只有声明放在头文件中,定义则位于dot-C文件中。
但是对于模板代码却会发生链接错误,针对这个问题,通常采用对待宏或内联函数的解决方法:把模板的定义也包含在声明模板的头文件中,即让定义和声明都位于同意头文件中。称模板的这种组织方式为包含模型,其带来的问题是:大大增加了编译复杂程序所耗费的时间。若不考虑创建期的时间问题,建议尽量使用包含模型来组织模块代码。
注:非内联函数模板与“内联函数和宏”有一个很重要的区别,那就是非内联函数模板在调用的位置并不会被扩展,而是当它们基于某种类型进行实例化之后,才产生一份新的(基于该类型的)函数拷贝。
二.显示实例化
C++标准还提供了一种手工实例化模板的机制:显示实例化指示符。显式实例化指示符由关键字template和紧接其后的我们所需要实例化的实体(可以是类、函数、成员函数等)的声明组成,而且,该声明是一个已经用参数完全替换之后的声明。可以显式实例化类模板,这样就可以同时实例化他的所有类成员。但是有一点需要注意:对于这些在前面已经实例化过的成员,就不能再次对它们进行实例化。
对于每个不同实体,在一个程序中最多只能有一个显式实例化体,换句话说,你可以同时显式实例化print_typeof<int>和print_typeof<double>,但是同一程序中的每个标识符都只能够出现一次。如果不遵循这条规则,通常都会导致链接错误,连接器会报告:发现了实例化实体的重复定义。
为了能够根据实际情况,自由的选择包含模型或显示实例化,可以把模板的定义和模板的声明放在两个不同的文件中。通常的做法是使用头文件来表达这两个文件(头文件大多是那些希望被#include、具有特定扩展名的文件);通常而言,遵守这种文件分开约定是明智的。
三.分离模型(导出模板)
1.关键字export:在一个文件中定义模板,并在模板的定义和(非定义的)声明的前面加上关键字export。
即使在模板定义不可见的条件下,被导出的模板也可以正常使用,换句话说,使用模板的位置和模板定义的位置可以在两个不同的翻译单元中。
实际上关键字export可以应用于函数模板、类模板的成员函数、成员函数模板和类模板的静态数据成员。另外,还可用于类模板的声明,这将意味着每个可导出的类成员都被看作可导出实体,但类模板本身实际上却没有被导出(因此,类模板的定义仍然需要出现在头文件中)。你仍然可以隐式或显式的定义内联成员函数。然而,内联函数却是不可导出的。
export关键不能和inline关键字一起使用;如果用于模板的话,export要位于关键字template的前面。
但其也存在的缺点:在应用分离模型的最后,实例化过程需要处理两个位置:模板被实例化的位置和模板定义出现的位置。虽然这两个位置在源代码中看起来是完全分离的,但系统却为了这两个位置建立了一些看不见的耦合。这就意味着编译器需要进行一些额外的处理,来跟踪所有的这些耦合。这也将导致程序的创建时间可能会比包含模型所需要的创建时间还要多。并且,被导出的模板可能会导致出人意料的语义。
注:当exported模板遇到编译错误,且提示要引用被隐藏代码的定义时,应如何处理?
1.对于我们预先编写的代码,存在一个可以在包含模型和分离模型之间相互切换的开关;在此,使用预处理指示符
来获得这种特性。另外,需重申的是除了明显的逻辑区别之外,这两种模型之间还具有细微的语义区别。
四.模板和内联
把短小函数声明为内联函数是提高运行效率所普遍采用的方法。inline修饰符表明的是一种实现:在函数的调用处使用函数体(即内容)直接进行内联替换,它的效率更优于普通函数的调用机制(针对短小函数而言)。
函数模板和内联函数都可以被定义于多个翻译单元中。
函数模板缺省情况下是内联的。
对于许多不属于类定义一部分的短小模板函数,应使用关键字inline来声明它们。
五.预编译头文件
当翻译一个文件时,编译器是从文件的开头一直进行到文件末端的。
预编译头文件机制主要依赖于下面的事实:我们可以使用某种方式来组织代码,让多个文件中前面的代码都是相同的。
充分利用预处理头文件的关键之处在于:(尽可能地)确认许多文件开始处的相同代码的最大行数。
有些程序员会认为:在使用预编译头文件的时候,允许#include一部分额外无用的头文件,要比只选择有用的头文件具有更好的编译速度:这还可以让包含策略的管理变得更加容易。
通常而言,预编译这个文件需要一段时间;但对于具有足够内存的系统,预编译头文件机制会使得处理速度比编译大多数单个(未经过预编译的)标准头文件快很多。
管理预编译头文件的一种可取的方法是:对预编译文件进行分层,即根据头文件的使用频率和稳定性来进行分层。
六.调试模板
1.理解长段的错误信息
2.浅式实例化
3.长符号串
4.跟踪程序
在确认程序可以正确运行之前,我们先要确认程序的创建过程也是成功的。
跟踪程序可以是一个用户定义的类,可以用作一个测试模板的实参。
5.oracles
在某些领域,tracer的一个扩展版本被称为oracles(或称为run-time analysis oracles)。它们是连接到推理引擎的tracers——所谓推理引擎(inference engine)是一个程序,他可以记住用来推导出结论的断言和推理。
6.archetype
利用archetype,我们可以验证一个模板实现是否会请求期待以外的语法约束。典型而言,一个模板的实现可能会为模板库中标记的每个concept,都开发一个archetype。
七.小结
1.模板给原始的“编辑器+链接器”模型带了挑战,因此,需要使用其他的方法来组织模板代码,这些方法是包含模型、显式实例化和分离模型。
2.在大多数情况下,你应该使用包含模型(就是说,把所有模板代码都放在头文件中)。
3.通过把模板声明代码和模板定义代码放在不同的头文件中,你可以很容易的在包含模型和显式实例化之间做出选择。
4.C++标准为模板定义了一个分离的编译模型(使用关键字export)。然而,该关键字的使用还没有普及,很多编译器也不提供支持。
5.调试模板代码是具有挑战性的。
6.模板实例化可能会具有很长的名称。
7.为了充分利用预编译代码,要确认#include指示符的顺序是相通的。
模板术语
一.“类模板”还是“模板类”
在C++中,类和联合都被称为类类型。如果不加额外的限定,我们通常所说的“类”是指:用关键字class或者struct引入的类类型。需要特别主义的一点就是:类类型包括联合,而“类”不包括联合。
对于称呼具备模板特性的类,现今还存在一些混淆:
1.术语类模板说明的是:该类是一个模板;它代表的是:整个类家族的参数化的描述。
2.另一方面,模板类通常被用于下面几个方面:
(1).作为类模板的同义词。
(2).从模板产生的类。
(3).具有一个template-id名称的类。
二.实例化和特化
模板实例化是一个通过使用具体值替换模板参数,从模板产生出普通类、函数或者成员函数的过程。这个过程最后获得的实体(譬如类、函数或者成员函数)就是我们通常所说的特化。
三.一处定义原则
现在,我们只需要记住下面的ODR基本原则就足够了:
1.和全局变量与静态数据成员一样,在整个程序中,非内联函数和成员函只能被定义一次。
2.类类型和内联函数在每个翻译单元中最多只能被定义一次,如果存在多个翻译单元,则其所有的定义都可以是等同的。
3.一个翻译单元是指:预处理一个源文件所获得的结果;就是说,它包括#include指示符(即所包含的头文件)所包含的内容。
四.模板实参和模板参数
模板参数是指:位于模板声明或定义内部,关键字template后面所列举的名称。
模板实参是指:用替换模板参数的各个对象。和模板参数不同的是,模板参数可以有不局限于“标识符名称”(就是有多种类型或值)。
一个基本原则是:模板参数必须是一个可以在编译期确定的模板实体或者值。