C++ 模板元程序(二)

第一章

你可以通过此章进行一下热身。由此也可以测试一下你所使用的工具,了解一些基本概念及术语。在本章结束时,你最起码已经有了本书所涉及内容的一个粗略概念,并且你要(我们也希望)继续往下学习。

1.1 让我们开始吧

模板元程序的一个好处就是与以前旧的良好传统系统共享一种特点,就是一性生就之后,只要其能如预期工作,就无需了解其底下是如何实现的。

为了让你确信这一点,可以来一小段用 C++ 模板写成的元程序:

#include "libs/mpl/book/chapter1/binary.hpp"
#include <iostream>

int main()
{
std::cout << binary<101010>::value << std::endl;
return 0;
}

就算你对于二进制的运算了如指掌,但是如果不实际执行上面的代码,你是无法知道其输出是什么的,因此我们依然不无叨扰地建议你在你的编译器上编译一下上面的小程序,一来可以增强你的自信心,二来呢也可以测试一下你所用的编译器处理本书中的代码的能力。这面的程序意思是输出二进制数值所对应的十进制数 101010:

42

到标准输出上。

1.2 那么,什么是元程序呢?

如果你将元程序(Metaprogramming)分拆开来看,在字面上的意思就是“程序的程序”[1],一个少了一点诗意的解释就是:元程序就是操作代码的程序。初听起来很怪的概念,但是其实你可以已经熟悉了其中的几个这样的巨兽了。你的 C++ 编译器就是一个很好的例子:它操作你的 C++ 代码,以产生汇编代码或者机器代码。

[1] 在哲学上,如上面的, 在 Programing 之前缀以 Meta 以来表示“关于”,或者“更高一级的描述”,这是从希腊语言“Beyond”或者“Behind”引申而来的。

还有由 YACC 生成的语法分析器,是另一个例子。YACC 的输入是根据一种语法规则的写成的并且将其行为也附加其上的高级别语法分析器描述语言。如用前向优先规则分析并求值某个算术表达式,我们可以将下面的代码输入给 YACC:

   expression : term
              | expression '+' term { $$ = $1 + $3; }
              | expression '-' term { $$ = $1 - $3; };
   term : factor
        | term '*' factor { $$ = $1 * $3; }
        | term '/' factor { $$ = $1 / $3; };

   factor : INTEGER
          | group;

   group : '(' expression ')';

相应地,YACC 将产生 C/C++ 源代码(夹在其他部分之中):一个是 yyparse 函数,此函数可以根据规则来对语法进行分析并执行相应的动作:[2]

[2]  这也表明,我们也实现了 yylex 函数来将文本进行符号化,第十章有更详细的例子。当然如果你够狠,可以打开 YACC 的手册。

   int main()
   {
     extern int yyparse();
     return yyparse();
   }

YACC 的操作大多在于语法分析区领域的设计,因此我们也将 YACC  的输入语言叫做这个系统的“领域语言”。由于用户的程序一般都需要一个通用的编程系统,并且必须与所生成的语法分析器进行交互。YACC 将“领域语言”转换成主语言,也就是 C,之后用户又将其与自己的其他代码进行连接。因此“领域语言”经过了两个过程的转换,而且用户也非常了解“领域语言”与其代码之间的边界。

1.3 主语言中的元程序

YACC 所转换的“领域语言”与其主语言,两者是不同的。更为有趣的元程序的形式出现于像 Scheme 这样的语言中。在 Scheme 语言中的元程序作用会定义一个语言子集,此子集是合法的 Scheme 程序,而元程序经过同一个转换步骤来处理用户的程序。程序员在二进制程序、元程序以及写“领域语言”之间进行来回穿梭,但却没有意识这种变换,他们可以将多个领域无缝地结合在一个编程系统中。

令人兴奋的是,如果你有一个 C++ 编译器,那么在你的指尖也有这样的能力,书的后面章节就是向你展示何时、何地以及如何来释放这样的威力。

1.4 C++ 中的元程序

在 C++ 中发现模板机制可以为我们提供功能丰富的本地语言的元程序纯属偶然。在这一节中,我们就展示在 C++ 元程序中所使用的一些基本技巧及手法。

1.4.1 数值计算

最早的 C++ 元程序就是在编译期进行数据计算。最早一个提交给 C++ 委员会的元程序则是由 Erwin Unruh 提供的。这实际是一个无法通过编译的代码段,而这些代码编译时出错的信息中输出了一系列的质数!

由于非法代码(无法通过编译)是无法在一个大型系统中有效应用的,那么我们再测试一下实际的应用。下面的元程序(这是我们这前出现的编译器测试代码中的核心部分)将无符号的整数转当成二进制表示,使得我们可以用我们可识别的二进制方式来表达某个十进制数:

   template <unsigned long N>
   struct binary
   {
       static unsigned const value
          = binary<N/10>::value << 1   // prepend higher bits
            | N%10;                    // to lowest bit
   };

   template <>                           // specialization
   struct binary<0>                      // terminates recursion
   {
       static unsigned const value = 0;
   };

   unsigned const one   =    binary<1>::value;
   unsigned const three =   binary<11>::value;
   unsigned const five  =  binary<101>::value;
   unsigned const seven =  binary<111>::value;
   unsigned const nine  = binary<1001>::value;

如果你还在疑惑:“程序在哪?”,我们提请你注意我们在访问类型 binary<N> 的内嵌成员 ::value 的地方。binary 模板会用更小的 N 来实例化,直到 N 变为零,而这将通过一个特化版本来进行条件的终结。这样的过程相当有递归函数的调用过程的味道。但是元程序还是一个函数?最终编译器就被我们用来解析了我们的元程序。

错误检测:

在上面的代码中,没有什么能够限制我们向 binary 输送一个 678 这样值,而这样的十进制数并不是一个有效的二进制表示,结果将是一个非常奇怪的结果(将会是:6x22 + 7x21 + 8x20)。但是无论怎样,678 这样的一个数表示了一种用户逻辑上的Bug,在第三章中,我们将向你层示如何保证使得十进制的表示中只含有 0 和 1。

由于 C++ 中揭示了在运行时以及编译时两种不同运算的明显区别,元程序就是运行时的对面,即编译时。如 Scheme 中,C++ 使用相同的语言像完成普通程序一样来完成元程序的编写,但是在 C++ 中只有编译时的语言子集才能用在元程序中。用下面的代码与上面的代码进行比较:

   unsigned binary(unsigned long N)
   {
       return N == 0 ? 0 : N%10 + 2 * binary(N/10);
   }

运行时与编译时关键不同在于结束条件的处理方式上。在元程序中当 N 为令时结束条件是通过模板特化来完成的。而这几乎是所有 C++ 的元程序所使用方式,尽管有时它隐在元程序库的后面。

而另一个不同在于,如下面这个例子中所提示的那样,我们将递归换成了一个 for 循环:

   unsigned binary(unsigned long N)
   {
       unsigned result = 0;
       for (unsigned bit = 0x1; N; N /= 10, bit <<= 1)
       {
           if (N%10)
               result += bit;
       }
       return result;
   }

尽管看起来比递归版本的代码要多一些,但是多数的 C++ 程序员还是喜欢这一个版本的代码。至少是因为运行时迭代要比递归更有效率。

而编译时的 C++ 版本由于与 Haskell 一样具有一个特征我们将其称为“纯函数式语言”,这就是:(元)数据是不可变的,(元)函数也没副作用。这导致C++编译时并没有与运行时函数中使用的 non-const 相对应的任何元素。因为你不能写一个在结束时不判断可变状态的循环(无限循环),迭代而很快就会超过编译时处理能力之外。因此,递归是 C++ 元程序的惯用法。

1.4.2 类型计算

比 C++ 能处理编译时期计算问题更强的能力是其可以在编译时期计算类型。而且实际上,类型的计算则正是本书所涉及的内容。在我们的下一章最前部分,我们将讨论一些例子。当我们看过些例子,你将会可能将模板元编程认为就是类型计算。

尽管你可能已经读了第二章的内容并理解了类型计算,我们也还会向你传达这种能力。回忆一下 YACC 中的数据计算表达式?C++ 元程序最终使我们不需要使用某个转换器去取得某种能力或者方便性。从 Boost 的Spirit 库中使用恰当的代码,下面的 C++ 代码就可以完成 YACC 数据计算表达式的功能:

  expr =
         ( term[expr.val = _1] >> '+' >> expr[expr.val += _1] )
       | ( term[expr.val = _1] >> '-' >> expr[expr.val -= _1] )
       | term[expr.val = _1]
       ;

   term =
         ( factor[term.val = _1] >> '*' >> term[term.val *= _1] )
       | ( factor[term.val = _1] >> '/' >> term[term.val /= _1] )
       | factor[term.val = _1];

   factor =
         integer[factor.val = _1]
       | ( '(' >> expr[factor.val = _1] >> ')' ) ;

每一个表达式都产生一个计算其右手边求值的分析及计算过程的对象,对赋给左边的对象。当被调用时,每一个对象的行为,都由生成这个对象的类型来决定的。而每个类型的表达式的计算则是由一系列的元程序与相关的操作符来完成的。

就如同 YACC,Spirit 元程序库是从语法规范来产生语法分析器的元程序。但是又与 YACC 不同的是,Spirit 所定义的语言是C++语言的一个子集。如果此时你还不能看明白这一切是如何发生的,不用担心,读完这本书后,你就会明白了。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
作者: 胡彦 本框架是一个lex/yacc完整的示例,用于学习lex/yacc程序基本的搭建方法,在linux/cygwin下敲入make就可以编译和执行。 本例子虽小却演示了lex/yacc程序最常见和重要的特征: * lex/yacc文件格式、程序结构。 * 如何在lex/yacc中使用C++和STL库,用extern "C"声明那些lex/yacc生成的、要链接的C函数,如yylex(), yywrap(), yyerror()。 * 重定义YYSTYPE/yylval为复杂类型。 * 用%token方式声明yacc记号。 * 用%type方式声明非终结符的类型。 * lex里正则表达式的定义、识别方式。 * lex里用yylval向yacc返回属性值。 * 在yacc嵌入的C代码动作里,对记号属性($1, $2等)、和非终结符属性($$)的正确引用方法。 * 对yyin/yyout重赋值,以改变yacc默认的输入/输出目标。 * 如何开始解析(yyparse函数),结束或继续解析(yywrap函数)。 本例子功能是,对当前目录下的file.txt文件,解析出其中的标识符、数字、其它符号,显示在屏幕上。linux调试环境是Ubuntu 10.04。 总之,大部分框架已经搭好了,你只要稍加扩展就可以成为一个计算器之类的程序,用于《编译原理》的课程设计。 文件列表: lex.l: lex程序文件。 yacc.y: yacc程序文件。 main.hpp: 共同使用的头文件。 Makefile: makefile文件。 file.txt: 给程序解析的文本文件。 使用方法: 1-把lex_yacc_example.rar解压到linux/cygwin下。 2-命令行进入lex_yacc_example目录。 3-敲入make,这时会自动执行以下操作: (1) 自动调用flex编译.l文件,生成lex.yy.c文件。 (2) 自动调用bison编译.y文件,生成yacc.tab.c和yacc.tab.h文件。 (3) 自动调用g++编译、链接出可执行文件main。 (4) 自动执行main,得到如下结果:。 bison -d yacc.y g++ -c lex.yy.c g++ -c yacc.tab.c g++ lex.yy.o yacc.tab.o -o main id: abc id: defghi int: 123 int: 45678 op: ! op: @ op: # op: $ AllId: abc defghi 参考资料:《Lex和Yacc从入门到精通(6)-解析C-C++包含文件》, http://blog.csdn.net/pandaxcl/article/details/1321552 其它文章和代码请留意我的blog: http://blog.csdn.net/huyansoft 2013-4-27

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值