數值計算優化方法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++(二)——表達式模板

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章