数值计算优化方法C/C++(一)——模板元编程

模板元编程

1、概述

模板元编程是使用C++编译时的模板推导能力进行数值、类型的运算推导的技术。最早的C++的源程序之一是Erwin Unruh在一次C++标准委员会会议上所展示的,那一段代码本身是不能通过编译的但在其编译时的错误提示信息中包含了一系列计算出来的指数值。模版元编程需要很多技巧,常常需要类型重定义、枚举常量、继承、模板偏特化等方法来配合,因此编写模版元编程比较复杂也比较困难。STL中广泛运用了这种技术,例如type_traits。boost中的mpl库则提供了更多的元编程工具。

2、简单使用

模板元编程的逻辑其实很像是递归过程,即在编译的时候不断填充模板参数、展开,直到最终没有继续需要填充和展开的模板时就得到了一个模板的实例。如何控制这一过程,让模板展开按照我们想要的方式进行,并返回我们需要的结果就是进行模板元编程所要考虑的问题。最简单的就是通过一个递归的模板加上一个特化实现一个简单递归过程。一提到递归,那当然是经典的斐波那契数列了,不过我们先不写这个,我来个更简单的阶乘运算。

阶乘

template<int n>
class factorial{
    public:
    enum{res=n*factorial<n-1>::res};
};

上述的函数实现了一个乘法的递归,每一个模板类的参数n都会和n-1的模板实例中的枚举类型成员res进行相乘,作为当前模板类的成员res的值。现在递归过程有了,接下来我们就需要一个条件让这个递归过程终止。所以我们就需要一个特化的模板,即当模板参数n为某个特定值是就不再和n-1的模板计算了,而是直接给res赋予一个合适的值。所以我们做如下特化。

template<>
class factorial<1>{
    public:
    enum{res=1};
};

这个特化模板是当模板参数n=1时,其对应的模板类中的枚举类型成员res为1。这时当前面的模板不断填充、展开到factorial<1>时就会得到一个确定的res而不再是一个需要继续填充、展开的模板了。这时再反算回去就可以得到最终的factorial的实例,而其成员res就是我们需要的阶乘结果。

接下来我们来测试一下这个阶乘的模板元函数

#include <iostream>
int main(){
    std::cout<<factorial<10>::res<<std::endl;
    return 1;
}

最终结果是3628800,刚好是10!。这就是最简单的一个模板元函数了。

接下来我们再做一个斐波那契数列的例子。

斐波那契数列

老样子,首先是一个递归的模板类

template<int n>
class Fibonacci{
    public:
    enum{f=Fibonacci<n-1>::f+Fibonacci<n-2>::f};
};

由于斐波那契数列终止时需要两个特化模板类,因此我们特化两个模板类

template<>
class Fibonacci<2>{
    public:
    enum{f=1};
};
template<>
class Fibonacci<1>{
    public:
    enum{f=1};
};

这样斐波那契数列的模板元函数我们也写好了,测试一下

#include <iostream>
int main(){
    std::cout<<Fibonacci<40>::f<<std::endl;
    return 1;
}

结果是102334155,恰好是斐波那契数列第40项的值。接下来我们看看使用元函数和不使用元函数到底有什么区别。

我们再写一个不使用元函数的斐波那契数列的程序

int Fibonacci(int n){
    if(n<3) return 1;
    return (Fibonacci(n-1)+Fibonacci(n-2));
}

然后都计算第40项的值,并分别编译运行。

使用元函数的程序计算40项斐波那契数列的用时是

real	0m0.002s
user	0m0.000s
sys		0m0.001s

而未使用元函数的程序用时是

real	0m0.480s
user	0m0.480s
sys		0m0.000s

可以看到相比于未使用元函数的情况,元函数的运行速度远高于未使用元函数的程序。这一点当然好理解,因为元函数的计算是在编译期进行的,程序一旦编译好函数值就已经得到了,执行过程相当于直接把结果输出到控制台,自然比计算的速度快的多了。这也就是模板元编程的一个作用,对于可以在编译期计算出结果的函数,使用元函数可以把计算提前到编译期,从而节省运行期的计算开销。但是相应的编译时所消耗的时间也会有所增加,如果程序只需要一次编译,但需要反复复用,那么使用元函数是一个不错的选择。当然C++11的constexpr关键字为我们省了不少事,不需要那么复杂的去写模板类,再去特化以达到编译器计算的目的,直接使用constexpr就好了。

引用移除

除了数值计算以外,模板元编程能做的还有很多。在使用类型推导的时候有时我们并不能得到我们真正想要的类型,例如对于int a[],我们使用decltype(a)时将会int[],而使用decltype(*a)或者decltype(a[0])将会得到一个int&,但是如果我们只想得到int类型怎么办呢?这时我们的元函数就可以出场了。

template <typename T>
class remove_reference
{
public:
   typedef T type;
};
template<typename T>
class remove_reference<T&>
{
public:
   typedef T type;
};

如果我们想通过一个T[] a变量来得到一个T类型就可以用如下形式

remove_reference<decltype(*a)>::type

这就是元函数的另一个作用,类型推导。

元函数还有一个经典的例子就是type_traits,这个例子我也没有写过,只是在书上和网上看了一些,所以也没有现成的代码,此外还有诸如奇特递归模板、enable_if选择分支等等一些内容。

上一篇:没有了
下一篇:数值计算优化方法C/C++(二)——表达式模板

发布了22 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/artorias123/article/details/86508559