模板可以被用做预编译程序,Todd Veldhuizen和David Vandevoorde指出,任何算法都能被模板化,算法的输入参数在编译期提供。只要有好的编译器,中间代码可以完全优化掉。

对斐波拉契数列的优化

斐波拉契数列,老生常谈啦,一开始学递归就学这个东西,通常下面这种方法都是明令禁止的:

unsigned int fib(unsigned int n) {
    if (n == 0 || n == 1)
    {
        return 1 ;
    }
    else
    {
        return fib( n - 1) + fib (n - 2); 
    }
}

原因很简单,它在运行的时候会不停压栈,容易引起栈溢出的情况。

但是有一种办法可以适用模板元编程来进行优化。很多人其实不知道模板可以作为虚拟编译程序,可以快速大量地创建优化代码。

此外,由于算法的输入参数是在编译期提供的,因此不会在runtime的时候进行重复的操作,这样一来可以达到非常高的效率。

那么该如何进行优化以上代码?

template <unsigned int N>
struct FibR 
{ 
    enum 
    { 
        Val = FibR< N-1 >:: Val + FibR::Val 
    }; 
};

template <>
struct FibR <0> 
{ 
    enum 
    { 
        Val = 1 
    };
}; 

template <>
struct FibR <1> 
{ 
    enum 
    { 
        Val = 1 
    };
};
#define fib(n) FibR::Val

这样一来,我们可以通过#define来调用这个模板。

std::cout << fib (4) << std::endl;

需要注意的是,模板函数实际上不是真正的函数——它实际上是一个枚举整数,在编译期递归生成。语句Val = FibR< N-1 >:: Val + FibR::Val虽然不是很常见,但是是完全合法的。

FibR定义为一个Struct,是因为它的数据默认都是public的。而Val采用枚举整数的原因是它可以预先就指定它的Value。

当然,有递归,当然就要有结束条件。在模板中处理基本情况的方法就是使用模板特化(template specialization)。

凡是由template <>标记的,就意味着这是模板特化。那么对于fib(4)来说,编译器是这么玩的:

fib (4)
= FibR< 4 >::Val
= FibR< 3 >::Val + FibR< 2 >::Val
= FibR< 2 >::Val + FibR< 1 >::Val + FibR< 1 >::Val + FibR< 0 >::Val
= FibR< 0 >::Val + FibR< 1 >::Val + FibR< 1 >::Val + FibR< 1 >::Val + FibR< 0>:: Val
= 1 + 1 + 1 + 1 + 1
= 5

注意这是编译器玩的东西,所有的输入都在编译期间确定,因此在最终,编译器生成的代码就是:

std::cout << 5;

这种方法是C++中的一种很有用的方法。有些时候对于某些指数级的运行时间的函数,死都不能降为常数级运行时,可以考虑使用这种编程方式。

这样一来就可以通过增加额外的编译时间来降低程序的执行时间。当然对于游戏来说,执行时间肯定比编译时间重要。

阶乘运算

通常的做法是:

unsigned int fact (n ) 
{ 
    return n <= 1 ? 1 : n * fact( n - 1); 
}

但是如果使用模板元编程,那么代码就是:

template < unsigned int N >
struct FactR
 { 
    enum { 
        Val = N * FactR::Val 
    }; 
};

template <> 
struct FactR < 1 >
{
    enum 
    {
        Val = 1
    };
}; 

#define fact(n) FactR::Val

就和斐波拉契数列一样,编译器会将最终的运算调用进行换算,也就是说降成了常数级的运行时间,这就是使用元编程的好处。

反思

模板元编程当然也存在一些缺点:

  • 编译时间的损失,当然这一点通常不会特别重要。我习惯在代码编译的时候上个厕所喝杯咖啡啥的……
  • 代码可读性有些损失,但是我们可以尽量避免,比如使用宏定义等。

模板元编程虽然很有意思,而且很高效,但是说实话在项目中,这种东西用的真的特别多吗(注:这篇博客写于2015年8月,现在可以回答这个问题了:在游戏中其实对于矩阵乘法等操作都可以用到模板元编程,因此它还是很有必要去掌握的)?我不禁陷入了沉思的大波之中……