一直以来,在大多数情况下,递归被看做是低效率的表现,从学习编程开始,就一直被教导,不用或至少少用递归。但在模板编程中,递归和模板偏特化联合,起了相当大的作用。可以说,没有递归,在很大程度上,模板编程便无从谈起。给我感觉是,在模板编程中,递归是唯一有效的手段……
一个很简单的例子如下:
该例子在编译期将无符号的十进制数字转换为等价的二进制数字。第一个template是递归主体,第二个template是递归的边界条件。从模板技术的角度来看,第二个是对一个模板的特化。在任一次迭代中,两个模板并不会同时被进行,编译器会根据当前条件选择最合适的一个模板。对编译器来说,这里并不存在什么递归之类,编译器所看到的只是:一个模板需要被实例化,而实例化之后,编译器又看到一个模板需要被实例化……于是有了递归。
可以想象一下,其实我们所需要的不过是最后的那一串二进制数字,但为了在“编译期”得到这数字,编译器额外的产生了很多以后永远不会用到的模板实例。从某一角度讲,我很质疑递归在模板编程的大量使用是否得当……
话题远了些。
最近看Loki库,和《C++设计新思维》。Loki中,对模板的运用有点让人晕眩的感觉。Loki中大量使用了模板偏特化,模板递归。这在Typelist中表现尤为突出。
Typelist本身只是一个关于型别的List而已,虽然自身的用处并没有我想象中的大,但Typelist几乎构成了整个Loki的基础。对Type的使用能到这个程度,我觉得完全登峰造极了。
Loki中,对Typelist的定义如下:
说得更确切点,这是我自己的定义。在Loki中,并没有:
而我个人觉得,如果一个template的形参中全是typename T,或者class T,不免有点让人混乱。但如果定义一个typelist关键字,用以在某些适当的地方取代typename 或者class,那样的话会更醒目。
例如:
IndexOf,在typelist中搜索某type,其第一个参数是一个typelist,第二个参数是一个type,用所定义的关键字typelist取代typename ,来更清晰的表达IndexOf的语义。
不过使用#define来定义typelist,其实不太好的,没有想到更好的办法。typename并不是一个type,不能使用typedef。
这让我想到不久前,我看到C#中有这样的代码:(原谅我还没来得及学习C#……)
(http://blog.zhaojie.me/2010/06/more-why-java-sucks-and-csharp-rocks-1-reddit-and-property.html)
该代码表示:“有一个User数组,我们要根据它的年龄进行排序”……无疑这样的表达更体现出了程序员的用意,即是体现了what而不是how。而在C++中,我们应该怎么写?我们可以写个方法,可以重载一个操作符,可以……而所有这些(至少是我所能想到的),都没有C#的这一句更能表达程序员的意图。
于是我想,是不是可以在C++自己创建一个运算符,比如=>来与C#类似的完成任务。答案是否定的,C++中不能新建运算符,其实好像缩语的语言都不能,但还是感到有些遗憾。比如也许我能将>>符号重载写出:users>>u.Age这样的代码,但怎么都觉得很奇怪……
语言都有语言的缺陷。C++总让我觉得是一门超大巨炮,什么都可以搞定。但在使用这门巨炮之前,得为这门巨炮铺一公里的代码……其实我本想完成的事情不过是排个序或者复制个数据罢了……
但用惯C++后,用其他什么都觉得很奇怪。比如我学python时,发现没有指针,没有typedef,我觉得相当不可思议……
说远了。再回来。
关于所#define 的TYPELIST_1,在Loki中一直写到了TYPELIST_50。用户在外部可以使用TYPELIST_50(...)来定义自己的一个有50个型别的typelist,但这样似乎暴露了我们内部的实现……(但在模板中,什么不是暴露的呢……),是不是有办法给出一个统一的接口,用户定义自己的typelist时可以不用写出TYPELIST_50这样神奇的代码,而只用给出他要typelist的型别即可。
Loki中有MakeTypelist,如下:
用户定义用:MakeTypelist<T1, T2, ...>::Result。看起来很复杂,其实是:递归+偏特化。第一个template在递归,而第二个是以偏特化的形式作为递归的终结条件的。最终当18个type都是NullType时,停止递归,也就完成了定义Typelist的任务。
不太明白的是,为什么只有18个Type?难道作者只允许定义18个?
在网上有看到另一种形式的MakeTypelist,是定义了一个50个Type的list,并使用即将介绍到的EraseAll来删除除最后一个NullType之外的其他NullType。不过觉得在Typelist这个递归+偏特化的场所显得不太合事宜……
接下来就是一系列的只在编译期进行call的模板函数。几乎全是递归+偏特化的机制:
本没有什么话说的。但在实际的Coding中,出了不少问题。template<...>中是参数,在其下的struct或函数中出现的参数必须出现在template的参数列表中。
想说一下的是模板片特化;我的理解是,模板偏特化即是显式地给出模板的参数,使之仅对某种情况适用。例如:
其中,struct Erase < NullType, _type >即是显式得给出typelist _list为NullType,从而实现偏特化。当编译器在进行模板实例化时,就会从所有的目标中(包括泛化和偏特化版本),选取最合适的那一个进行实例化。
泛化版本和偏特化版本在模板实例化中扮演的角色可以看做是:泛化是递归体,偏特化是边界条件(即是非递归体,一个确定体)
所有这上边的,都是对型别进行操作。所作的基本是基础性的工作。下面要说的是,用所有这上边的,来做点实际能有点作用的,实际可以在运行期留下足迹的东西。
进行class的模板级Create操作:
Class产生在编译期,可能被运行期使用。产生的方式是通过继承,和对继承的实例化。这实际上也是一个模板递归的过程。在这里,atomic type是边界。
既然产生出了class,那么剩下的事就是从这么一堆class中选择正确的class了。由于涉及到,可能在typelist中存在重复的type,因此Loki中作出了一个Helper。如下:
在这里使用了Int2Type,通过Int2Type来构成另一个参数,Int2Type< 0 >是边界条件。
Typelist让我想起了《数据结构》中的广义表。二者太像了。都是分为头尾,都是递归的广泛使用。
最后是我的测试用例。其中使用了Typeinfo的外覆类。