zhangJW_cn 阅读(1028) 评论(12)
    前几天看了荣耀的《C++模板元编程技术与应用》演讲,提到了元编程的一些好处和不利。

    由于模板具备展开的特性,特化提供了选择的能力,因此很容易得到以下好处:
    1. 编译期计算
        比如我们计算Fibonacci数列,就可以避免以往的运行期计算,直接在编译期由编译器计算出需要的结果。
    2. 编译期选择
        作者给出了通过条件选择两个模板类型之一的示例。

    并能在以下方面取得较好的应用前景
    1. 编译期结构控制
        因为具备了特化选择的能力,很容易实现编译期的if流程;加上模板展开能力,将非常容易的做到for等的循环开 解;条件的选择,同样相当方便的就做到了switch。
    2. 编译期数据结构
        数据的存储,同样可以通过特化能力进行分支选择。
    3. 数值计算
    4. 类型计算
    5. 代码生成
    6. 编译期断言和约束
        通过不存在的展开代码和条件,导致编译无法通过,从而实现断言。
    7. 库设计
    8. DSEL(domain-specific embedded language)设计

    模板元编程的不利:
    1. 代码的可读性较差
    2. 调试困难
    3. 编译时间延长
    4. 结果程序性能未必一定最优化
    5. 编译器局限性
    6. 可移植性较差

    作者准确、严格、精确的描述,给我们指出了一片广阔的空间。C++本身就是一个程序员负责的语言,程序员的稍有大意,就会造成无法预期的结果。我们生成的代码,编写的程序、软件,本身就是交给机器去运行的,不管是直接在硬件上执行,还是在VM上运行。元编程无疑是将部分代码交给了编译器去解释执行。
     1. 编译期计算真的那么重要?
        拿作者给出的Fibonacci数列,编译器的解释,为什么不能在运行期获得,而直接赋值呢?一个是编译器先解释,一个是另一个程序先运算。通常我们垢弊的就是直接赋值可读性差,元编程天生的毛病这里也有。当我们对所赋值有着提示性的注释 时,结果并全然不一样了。
    2. 编译期选择真的那么重要?
        这一技巧性的特化所得到的结果最终是固定的代码,直接也可以写出我们需求的这个固定的代码。一个是我们编写给机器生成的代码(我们间接编写),一个是我们直接编写的代码。

    对应更具体的应用,带给了我们什么?
    1. 编译期结构控制
        结构流程的控制,本来是我们控制程序运行的。编译器的流程控制:If选择流程,除了一层奇怪的包装,并没有带来更清晰的流程,更高的效率;开解的循环,同样对运行期无能为例,对编译时本身就固定的循环,只是比手动的开解书写的更快, 但却带来了奇怪的语法;switch跟是奇怪的语法。它们带来的,究竟是什么?除了新的奇怪的语法(其实也不新了,从模板的引入, 这种语法就已经诡异的出现在了我们面前),恐怕更难有明显的好处。
    2. 编译期数据结构
        把本来就复杂的结构,变得更扑朔迷离,诡异难辨。
    3. 数值计算
        元编程对数值的计算,主要体现在编译器的展开解释上,预计算的元代码改为运行期代码,最终需要的结果也一样。同样是编程,编写运行期的这段代码,由于已有的经验和较少的技巧,大多数人可以更快的编写;对元程序需要的编译时间,运行 期的编译显得更少,多的只是新建工程的时间;对于C++标准要求的至少12次迭代能力,运行期显得更为宽松自由。
    4. 类型计算
        数据类型通常是程序员手动编写的,对程序员而言并不需要判断,尤其是C++这种由程序员负责的语言。
    5. 代码生成
        在遇到大量类同相似的代码时,为了减少编码时间,我通常采用宏生成代码。同样的调试困难,但清晰的宏将使得你一看便知。对于采用编译期分支、展开能力生成代码,可读性不佳并难以调试,我更倾向于手动去编写这部分代码。
    6. 编译期断言和约束
        采用特化的技巧,使得编译时失败,面对编译器里输出的那一堆信息,通常要查找上半天,才能知道问题所在。对于C/C++程序员,我们会确保表达式求值的正确。去编写一个static_assert,并没有习惯了自身确保表达式求值正确,更快捷,效果 可能也不太那么明显。当然,当编译器提供了这一支持时,对我们来说,也可带来适度的便利。
    7. 库设计
        在C++,已经漫天飞舞着template的今天,库设计,可能是元编程带来的好处可以被极大发挥和应用的地方。
    8. DSEL设计
        采用C宏编写脚步的例子以前也曾见过。

    由于以上的优点和缺点,而这些优点在通常编程中并不明显,估计也就是今天我们看到的元编程的现状结果。

    附:我见过一个元编程实现的max函数,不过,相对于宏,我委实没有发现它的好处所在。

评论列表
小明
re: 关于“元编程”的浅思考
C++元编程有些太复杂,但是确实也很强大,就是普通程序员用到的可能性不大。

代码生成方面,Java中XDoclet和类似于Velocity之类的模板引擎,学起来简单一些,功能也不弱。
Diviner
re: 关于“元编程”的浅思考
任何把C++变得很复杂的做法我都反对。这东东有点hacker的味道,但却又价值不大。
清风雨
re: 小明、Diviner
小明:
看了你对java还很有研究啊!我只知道java的语法。现在的范型java,由于长期没有学习过了,估计现在连语法都不会了。

Diviner:
我和你对这个的感觉很类同。我总觉得模板的引入可能是C++的一个败笔。不过,自己了解不深,所以,只能感觉。
周星星
re: 关于“元编程”的浅思考
我没有看出模板元编程有任何不利:

1. 代码的可读性较差
2. 调试困难
3. 编译时间延长
--- 想吃饭就得张嘴,张嘴也很累,但不能说吃饭有一个不利点:累。因为你完全可以不去吃。如果是因为C++提供了元编程的能力,使得不使用元编程的代码其可读性也变差了,调试也变困难了,编译时间也变长了,那才可以说这是元编程的一个缺点。

4. 结果程序性能未必一定最优化
--- 我就想不通了,既然未必一定最优化,那你为什么要这么设计?
随手拿个工具就使用,也不先了解一下这个工具适不适合现在的这个工作需要,却反口责备工具不行。

5. 编译器局限性
6. 可移植性较差
--- 现在说的是C++的元编程,还是说的是某一个编译器特有的元编程?如果是前者,那么一切都按照标准C++规定的语法来,既然是标准C++,又哪来的编译器局限性和可移植性较差?

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

如果C++可以增加编译期代码执行能力的话,那么元编程就是不惜要的,比如
<--
int foo( int m, int n ) { return pow(m,n); }
-->
<-- if( 123 == foo() ) -->
            fun1();
<-- else -->
            fun2();
但要知道元编程是在不更改C++原有语法的前提下实现的编译期代码执行能力,所以 元编程 和 那个XX语言的YY功能 是不可以进行比较的,因为存在基础不一样。
局部变量
re: 关于“元编程”的浅思考
我需要的是“元元编程”和“元元元编程”:)
Diviner
re: 关于“元编程”的浅思考
反正我的建议是在实际的项目中尽量少用模板(标准库的除外),坚决不用比较复杂的模板(连阅读都成问题的),元编程更是否决。
清风雨
个人看法
我觉得除非做库设计,甚至做库设计也最好少用模板。
别的方面,尤其是实际应用中,最好是不要用模板。
已有的STL,因为已经是标准,采用使用,而不是开发的心态来利用。

对于模板的范型,个人更倾向尽量的用继承和多态来完成。
对于模板的特化,不妨也采用多态来努力。(不过,我打出了多态后,自己犹豫了,当我google以后,也确实值得犹豫了,overload是否该算Polymorphisn? 这个是题外话了。)
周星星
re 清风雨:
建议你看看《C++设计新思维》,不是想让你见识一下模板的伟大,而是想让你知道 (模板) 和 (继承和多态) 是完全不同功能的方法。对于一个用户需要的功能,你可以不使用(模板),也可以不使用(继承和多态),但(模板)实现不了任何(继承和多态)的功能,同时(继承和多态)也实现不了任何(模板)的功能。
《C++设计新思维》讲得很清晰,哪些能力和数据是(继承和多态)可以提供的,哪些能力和数据是(模板)可以提供的。
清风雨
re: 周星星
你觉得在哪些情况最好采用继承和多态,哪些情况最好采用模板呢?具体些,谢谢!
diclogic
re: 关于“元编程”的浅思考
《C++设计新思维》推荐看一下。
>>1. 代码的可读性较差
>>2. 调试困难 
个人觉得尽量轻量化工具化会比较好,诚然某些复杂的compile-time逻辑会出现不易维护的弊端,但将这样的“编译期工具”实现得尽可能功能单一且范适,那么我们可以用严密的unit test来保证它的质量。这样我们在调试的时候就可以降低对他的怀疑程度,就像我们通常会将对c库函数的怀疑放在最后一样。
>>3. 编译时间延长 
既然是compile-time的逻辑,会延长编译时间是理所当然的,但比起运行时间的延长我猜大多数人宁愿选前者。。。
>>4. 结果程序性能未必一定最优化
如果是一些compile-time数值工具那么通常会得到常数结果,如果是compile-time多态那么通常会得到一个inline call(可能是一个硬jump也可能是直接将代码内嵌),仅就这两种较常见的应用来说效率显然是提高的。当然任何获益通常都是有代价的--我们牺牲了run-time活动性。
>>5. 编译器局限性
>>6. 可移植性较差 
这两个问题应该算是同一个问题,既然template成为了标准,我们只能期望慢慢地他会被广泛支持。记得好像gcc支持对经过预处理期的中间代码的输出,不知道会不会支持对template处理后的中间代码的输出,如果能的话就不担心局限性问题了。

滥用run-time多态一样会造成不易维护的代码,所以问题出在“滥用特性”上而不是特性本身。

以上观点有不对的地方请指正
周星星
re diclogic:
您的观点和俺一致
刘未鹏
re: 关于“元编程”的浅思考
我现在越来越觉得mp的作用主要在于构建库组件的interface上面。宽泛一点来讲,就连DSEL也是一种interface.另外,元编程在构建active library方面很可能会一展宏图(实际上Blitz++就是一个Active Library)。C++应该算是主流语言里面第一个实现真正意义上的Active Library的语言,而这正归因于mp的编译期图灵完备能力。

发表评论
切换编辑模式