Andrew's Blog

Make things as simple as possible, but no simpler -- Albert Einstein

导航

对C++模板元编程有一些了解的朋友,想必都会有这样的感觉:为了达到某种目的,动辄
就要使用模板的递归定义,一个模板的定义之中嵌套着自身的实例化。比如,为了计算
n的阶乘,我们必须如此定义:

template <int N> struct Factorial
{
    enum { value = N * Factorial<N-1>::value; };
};
template <> struct Factorial<0> // partial template instantiation
{
    enum { value = 1; };
}

int main ()
{
    std::cout << "5! = " << Factorial<5> << std::endl;
}



一般的程序设计教材都会将计算阶乘作为讲解递归函数的典型范例。因为在数学中,阶乘的定义本身就是递归的:

n! = | 1                 当n = 0 时
        | n*(n-1)!     当n > 0 时

于是,我们可以很自然地写出下面的递归函数:

Titleint Factorial(int n)
{
    if (n == 0) return 1;
    return n * Factorial(n-1);
}

同时,也可以把该递归函数轻而易举地转化为迭代计算(即使用循环语句),例如:

int n = ...;
int factorial = 1;
for (int i = 1; i <= n; ++n)
    factorial *= i;

这样一来,就引出了一个问题:能不能在模板元编程中也使用迭代的方式来计算阶乘呢?
毕竟在有些情况下,迭代比递归要简便直观。

遗憾的是,答案是否定的。为什么?现将原因分析如下:
C++模板元编程与普通的编程方式不同,它属于编译时刻(Compile-time)的程序设计,
而不是运行时刻的程序设计。
在C++中,我们进行编译时刻程序设计的手段主要有三个:
(1)模板:更确切地说,是模板的特化(Specialization),通过定义主模板、完全
特化模板和部分特化模板,我们获得了在编译时刻编写if-else语句的能力,但是并没有
获得编写循环语句的能力。
(2)编译时刻常整数的计算:让我们获得了在编译时刻进行求值计算的能力,但是,
有一点十分重要,所有在编译时刻计算出来的数值都是不可再改变的。例如,在定义了一个
枚举值之后你就不能再去改变它。
(3)typedef语句:可以为已知类型引入别名,但是,一旦别名定义之后,你也不能再去
改变它。
显然,以上三条都与迭代操作格格不入,迭代操作要求存在一个迭代器(即循环变量),然后
不断的改变该迭代器的值,直到满足某个条件位置(这时循环中止)。由于在编译时刻程序
设计中,根本找不到可以改变其值的实体,因此,无法实现迭代操作。

最后,得出的结论是:虽然C++是一种命令式程序设计语言,但是在模板元编程中,为了
进行编译时刻计算求值,不得不大量采用递归的方法。这倒是有些像纯函数式程序设计语言,
比如说Haskell,因为在这类语言中根本不存在变量的概念,所以更谈不上什么迭代了。