现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

漫谈C++模板元编程的应用

2016-06-24 21:02 工业·编程 ⁄ 共 3735字 ⁄ 字号 暂无评论

    为了谈应用,先谈谈使命。模板元编程的根在模板。模板的使命很简单:为自动代码生成提供方便。提高程序员生产率的一个非常有效的方法就是“代码复用”,而面向对象很重要的一个贡献就是通过内部紧耦合和外部松耦合将“思想”转化成一个一个容易复用的“概念”。但是面向对象提供的工具箱里面所包含的继承,组合与多态并不能完全满足实际编程中对于代码复用的全部要求,于是模板就应运而生了。

    模板是更智能的宏。模板和宏都是编译前代码生成,像宏一样,模板代码会被编译器在编译的第一阶段(在内部转,这点儿与预编译器不同)就展开成合法的C++代码,然后根据展开的代码生成目标代码,链接到最终的应用程序之中。模板与宏相比,它站在更高的抽象层上面,宏操作的是字符串中的token,然而模板却能够操作C++中的类型。所以模板更加安全(因为有类型检查),更加智能(可以根据上下文自动特化)……

    说完模板,来说说模板元编程。模板元编程其实就是复杂点儿的模板,简单的模板在特化时基本只包含类型的查找与替换,这种模板可以看作是“类型安全的宏”。而模板元编程就是将一些通常编程时才有的概念比如:递归,分支等加入到模板特化过程中的模板,但其实说白了还是模板,自动代码生成而已。

    说完使命,来看看应用:编译时计算,补充类型系统,Domain Specific Language(是你说的“开发新语言”么?)

    编译时计算,比如拿模板来计算菲波纳切数列。优点是不占用运行时的CPU时间。但是这事儿吧,我觉得不该拿模板来搞,哪怕你拿python算好了再贴到C++文件里面,都比用模板好一点儿吧……还好C++11好像改了改这里,以后这种需求应该可以用constexpr来搞定了,那样会更好。

     补充类型系统,比如boost还是哪个的文档里面举的物理量计算的量纲问题。这个我觉得是非常有意义的,也是最有实用价值的。模板提供了参数化的类型,给我们一种来补充C++自带的类型系统的方法,使得类型系统更加智能与完备,很强大。

    DSL,我觉得用C++搞DSL不太好吧,有其它语言对DSL提供更易用的支持呀,比如scala, Haskell, Lisp……天涯何处无芳草,何必非跟C艹搞(我明明是C++脑残粉呀,似乎不能这么说吧,罪过罪过)……

    模板元编程缺点也是显而易见的,有人说它是C++里面的函数式编程语言,我觉得也有道理,维基百科上面说模板是图灵完全的,也就是理论上可以写出任何算法。然后这些信息综合一下儿就是尼玛命令式语言里面藏着一个函数式语言,一个编程语言里面放着另一个编程语言,这尼玛绝对是唯恐天下不乱的节奏呀!

    总而言之,这东西真有用,不信你出门问问,现在模板或宏已经成主流语言的标配了。但是这东西真的别乱用,想好了再用,码农何苦为难码农。如果你不觉得它有什么应用,就先别用。它不狭窄,是你还没看开……

-------------------------------------------------

从我的经验来看,C++的模板元编程(TMP)的基本原则就是:将负载由运行时转移到编译时,同时保持原有的抽象层次。其中负载可以分为两类,一类就是程序运行本身的开销,一类则是程序员需要编写的代码。前者可以理解为编译时优化,后者则是为提高代码复用度,从而提高程序员的编程效率。

我在实际工作中使用到的TMP举例如下:

1) 使用TMP进行loop unrolling 其中模板的参数决定了循环的长度。这个技术是很多地方都有用到的 我是在用CUDA写device端的程序时即需要利用loop unrolling优化,又想保留一个方便的接口在调试时修改,所以才用TMP。

2) 使用functor和operator将逻辑操作和实际运算分开,提高开发新算法的效率。比如如果一个新的算法可以复用operator,那么就编写新的functor作为模板参数传进去。基本上任何基于模板的算法库都会采用这种策略来提高代码复用率,比如boost。

3) 静态的多态。很多时候多态里子类的类型都是编译时可确定的,这时候采用通常的多态构造虚函数表其实是增加了运行时开销,利用模板将子类类型名作为模板参数放到父类里即可将运行时的负载转换为编译时负载。

将TMP运用到极致的库叫做Loki[1],我本人完全没用过,但阅读其中的代码可以加深对TMP的理解。另外,它里面提倡的policy-based design我在工作中经常使用。

当然,我感觉采用TMP的工业级代码还是相当少的,因为第一是冗长的编译时间开销会让一些调试和开发变得效率低下,另外TMP也会一定程度上降低程序的可读性。

---------------------------------------------

当今各种编程语言,相比于90年代,就干了两件事:

1.把本该发生在编译阶段的一些工作挪到运行期

2.把本该发生在运行阶段的一些工作挪到编译期

这两个方向分别代表着两种思维方式,即更自由还是更高性能. 1.的代表就是各种动态编译的脚本语言,而2在我看来最典型的代表就是C++的模板元编程了,但其实,模板元编程对大多数人来说确实用处不大,因为性能在今天对于大多数人已经不是最重要的了(不是最不重要的我就感到很欣慰了...),但是,毕竟有一些情况我们还是需要性能的. 举个简单的例子:

假如我们用float**代表着一个多路的一维信号,那么假如我们要对两个这样的多路信号进行拷贝,

用C++,一般我们这样写:

//float** in; float** out;for( size_t ch=0 ; ch<channelNum ; ++ch ) { for( size_t i=0; i<length ; ++i ) { out[ch][i]=in[ch][i]; }}

这么写挺标准,没什么问题,但是,作为一个性能狂魔,我表示不爽.

首先,如果在编译期我能确定channelNum是个确定的值,比如说是4,那么上面的代码可以优化成:

for(size_t i=0;i<length;++i) { out[0][i]=in[0][i]; out[1][i]=in[1][i]; out[2][i]=in[2][i]; out[3][i]=in[3][i];}

这样做的好处是至少有两个,1.优化了分支, 2.优化了数据缓存刷新 .带来了性能提升,但代码被写死了,channelNum必须是4,万一我需要3,或者5怎么办? 这时模板元就派上用场了:

template<int count>class Copy{public: static inline go( float* const out , float* const in ,int i) { Copy<count - 1>::go( out , in , i ); out[count][i-1] = in[count][i]; }};template<>class Copy<0>{public: static inline go( float* const, float* const, int) {}};

这样,无论cout参数为多大,每当运行"go"函数时,go必然要再次运行Copy<cout-1>::go.

这是一个递归结构,我就用它构建了一个能够自动生成代码的模板类"Copy",然后我这样写:

template<int channelNum>void palll_copy(float** out,float** in,size_t length){ for(size_t i=0;i<length;++i) { Copy<channelNum>::go(out,in,i); }}

这样,无论3个,4个,5个channel,我都能马上创建出相应的代码了.

(以上例子好像用boost::preprocessor更合适,只是举个例子)

然后,还有一些技巧,均是把运行期的工作转移到编译期.比如,我有个"施瓦星格"类,施瓦星格

比较壮,可以携带各种武器,我有个"武器类",其中有个纯虚函数"开火",一般来讲,在运行期我才能知道确定施瓦星格携带多少武器. 但这些武器是同时开火的. 针对这种需求,使用C++的虚函数和继承

显然是一种非常好的选择,一般也是这么干的.但是,有些情况下我在编译期就能确定施瓦星格携带

多少种武器,那么最方便的办法就是:

class 施瓦星格

{

public:

private:

刀 m_刀;

机枪 m_机枪;

..............

}

这么写,省去了虚函数的调用代价,也省去了一堆new,但我既要,又要自由,施瓦星格万一不带机枪了带霰弹枪怎么办? 这里就又能用模板元了,使用类型容器(代码太多..不写了),详见boost::MPL,写出这样的代码:

typedef 施瓦星格<刀,枪,长矛,激光枪,激光剑> 超级施瓦星格;

综上,模板元(至少对我来说),最大的好处就是,当你需要一个极端追求性能,甚至连一般的C++代码的性能都满足不了你的时候,请用.......汇编........ 但是当你既要更强的性能,又要更大的自由度和代码复用,当你妄图榨干编译器的最后一点点节操,当你妄图虐待编译器使其经常弹出"模板上下文太复杂".当你是如此有理想,以至于不惜使编译时间狂涨10倍,换来用户0.1毫秒的性能提升时,可以考虑模板元!

给我留言

留言无头像?