模板可以被用做预编译程序,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月,现在可以回答这个问题了:在游戏中其实对于矩阵乘法等操作都可以用到模板元编程,因此它还是很有必要去掌握的)?我不禁陷入了沉思的大波之中……