第一章
你可以通过此章进行一下热身。由此也可以测试一下你所使用的工具,了解一些基本概念及术语。在本章结束时,你最起码已经有了本书所涉及内容的一个粗略概念,并且你要(我们也希望)继续往下学习。
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++语言的一个子集。如果此时你还不能看明白这一切是如何发生的,不用担心,读完这本书后,你就会明白了。
C++ 模板元程序(二)
最新推荐文章于 2022-07-11 13:40:32 发布