0。前言 C里的模板能做什么呢?它好比C语言中的宏、C和Java中的自省(restropection)和反射(reflection),是C语言的外延。更极端一点地理解:它是一门新的图灵完备的编程语言(也就是说,C模板能实现图灵机模型里的全部功能)。在《ModernCDesign》中,作者抛出了以下几个问题: (1)如何撰写更高级的C程式? (2)如何应付即使在很干净的设计中仍然像雪崩一样的不相干细节? (3)如何构建可复用组件,使得每次在不同程式中应用组件时无需大动干戈? 解决上述问题的方法就是模板元编程。元(meta)本身就是个很抽象(abstract)的词,因为它的本意就是抽象。元编程,也可以说就是编程的抽象。用更好理解的说法,元编程意味着你撰写一段程序A,程序A会运行后生成另外一个程序B,程序B才是真正实现功能的程序。那么这个时候程序A可以称作程序B的元程序,撰写程序A的过程,就称之为元编程。 C中,元编程的手段,可以是宏,也可以是模板。 在继续阅读下面的内容之前,请先回顾下我之前的文章: C类模板和实例化 C性能优化系列专题1:优化string的使用(上) C模板元编程系列专题1:入门1、为什么需要泛型编程:从宏到模板,再到元编程 如果元编程中所有的变量(或者说元编程的参数),都是类型,那么这样的编程,我们有个特定的称呼,叫泛型。 模板的发明,仅仅是为了做和宏几乎一样的替换工作吗?可以说是,也可以说不是。 一方面,模板可以用来替换类型,这点和宏没什么区别。只是宏在编译阶段基于文本做纯粹替换,被替换的文本本身没有任何语义。而模板会在分析模板时以及实例化模板的时候都会进行检查,而且源代码中也能与调试符号一一对应,所以无论是编译时还是运行时,排错都相对简单。 另一方面,模板和宏也有很大的不同,模板最大的不同在于它是可以运算的。我们来看一个例子:voidAdd(uint8t,unit8t){} 上述函数实现了一个uint8t和uint8t类型的加法运算,如果现在要实现int16和int16类型的加法运算,该怎么办呢?简单点的方法如下:if(type8){Add(uint8t,uint8t)}elseif(type16){Add(uint16t,uint16t)} 但是这里有两个难点:首先,if(typex)是不存在于C中的其次,即便存在获取变量type的方法(Boost。Any中的typeid),我们也不希望它在运行时判断,这样会变得很慢。是否可以不引入ifelse,在编译期就把Add的方法确定呢? 有人说,重载、虚函数也能解决如上问题:voidAdd(uint8t,uint8t){}voidAdd(uint16t,uint16t){} 甚至在C语言中定义新的结构体Variant或使用void也能解决该问题:structVariant{union{uint8x;uint16y;}uint32ttypeId;}; 没错,但是如果我还有uint9t、uint10t等各种类型的加法运算呢?Anyway,不管是哪种方法都很难避免ifelse的存在。 模板与上述这些方法最大的区别在于:模板无论其参数或者是类型,它都是一个编译期分派的方法。编译期就能确定的东西既可以做类型检查,编译器也能进行优化,砍掉任何不必要的代码执行路径。2、类模板的特化:模板世界里的ifelse2。1根据类型执行代码 我们先来看看一个模板的例子:templatetypenameTTAddFloatOrMulInt(Ta,Tb);我们希望这个函数在T是float的时候做加法运算,在T是int类型的时候做乘法运算 那么当传入两个不同类型的变量,或者不是int和float变量,编译器就会提示错误。 从能力上来看,模板能做的事情都是编译期完成的。编译期完成的意思就是,当你编译一个程序的时候,所有的量就都已经确定了。比如下面的例子:inta3,b5;VariantaVar,bVaVar。setInt(a);我们新加上的方法,怎么实现的无所谓,大家明白意思就行了。bVar。setInt(b);VariantresultAddFloatOrMulInt(aVar,bVar); 从上述代码中我们可以看到:aVar和bVar都一定会是整数。所以如果有合适的机制,编译器就能知道此处的AddFloatOrMulInt中只需要执行int路径上的代码,而且编译器在此处也能单独为int路径生成代码,从而去掉那个不必要的if。在模板代码中,这个合适的机制就是指特化和部分特化(PartialSpecialization),后者也叫偏特化。2。2、如何写模板特化的代码 1。0版本伪代码intfloatAddFloatOrMulInt(a,b)类的静态函数{if(typeisint){}elseif(typeisfloat){}}voidfoo(){floata,b,c;caddFloatOrMulInt(a,b);intx,y,z;zaddFloatOrMulInt(x,y);} 2。0版本函数重载floatAddFloatOrMulInt(floata,floatb){}intAddFloatOrMulIntDo(inta,intb){}voidfoo(){floata,b,c;cAddFloatOrMulInt(a,b);intx,y,z;zAddFloatOrMulInt(x,y);} 3。0版本纯模板这个是给float用的。templatetypenameTclassAddFloatOrMulInt{TDo(Ta,Tb){}};这个是给int用的。templatetypenameTclassAddFloatOrMulInt{TDo(Ta,Tb){}};voidfoo(){floata,b,c;我们需要cAddFloatOrMulIntfloat::Do(a,b);。。。觉得哪里不对劲。。。啊!有两个AddFloatOrMulInt,class看起来一模一样,要怎么区分呢!} 好吧,问题来了!如何要让两个内容不同,但是模板参数形式相同的类进行区分呢?特化!特化(specialization)是根据一个或多个特殊的整数或类型,给出模板实例化时的一个指定内容。 4。0版本模板特化首先,要写出模板的一般形式(原型,即初始化,不能省)templatetypenameTclassAddFloatOrMulInt{staticTDo(Ta,Tb)注意这里必须得是静态方法!!!{returnT(0);}};其次,我们要指定T是float时候的代码:templateclassAddFloatOrMulIntfloat{public:staticfloatDo(floata,floatb){}};再次,我们要指定T是int时候的代码,这就是特化:templateclassAddFloatOrMulIntint{public:staticintDo(inta,intb){}};intfoo(){returnAddFloatOrMulIntfloat::Do(1。0,2。0);}intmain(){std::coutfoo();输出结果3。0} 解释:我们这个模板的基本形式是什么?templatetypenameTclassAddFloatOrMulI但是这个类,是给T是int的时候用的,于是我们写作classAddFloatOrMulI当然,这里编译是通不过的。但是它又不是个普通类,而是类模板的一个特化(特例)。所以前面要加模板关键字template,以及模板参数列表template这里要填什么?classAddFloatOrMulI最后,模板参数列表里面填什么?因为原型的T已经被int取代了。所以这里就不能也不需要放任何额外的参数了。所以这里放空。templateclassAddFloatOrMulIntint{。。。针对int的实现。。。}Done! 至此,第一个模板特化的代码已经写完了。这里的AddFloatOrMulInt如同是一个函数,却只能在编译期间执行。如果你体味到了这一点,那么恭喜你,你的模板元编程已经开悟了。3、总结 本文核心只讲了两个问题:一是为什么需要泛型编程,重点介绍了宏、模板和元编程的关系;二是模板类的特化代码如何编写。关于特化,还有很多细节知识,在之后的文章中我们继续探究,另外将还介绍偏特化等知识点,敬请期待。