淺談C++模板元編程
所謂元編程就是編寫直接生成或操縱程序的程序,C++ 模板給 C++ 語言提供了元編程的能力,模板使 C++ 編程變得異常靈活,能實現(xiàn)很多高級動態(tài)語言才有的特性(語法上可能比較丑陋,一些歷史原因見下文)。模板元編程的根在模板。模板的使命很簡單:為自動代碼生成提供方便。提高程序員生產(chǎn)率的一個非常有效的方法就是“代碼復用”,而面向?qū)ο蠛苤匾囊粋€貢獻就是通過內(nèi)部緊耦合和外部松耦合將“思想”轉(zhuǎn)化成一個一個容易復用的“概念”。但是面向?qū)ο筇峁┑墓ぞ呦淅锩嫠睦^承,組合與多態(tài)并不能完全滿足實際編程中對于代碼復用的全部要求,于是模板就應運而生了。
模板是更智能的宏。模板和宏都是編譯前代碼生成,像宏一樣,模板代碼會被編譯器在編譯的第一階段(在內(nèi)部轉(zhuǎn),這點兒與預編譯器不同)就展開成合法的C++代碼,然后根據(jù)展開的代碼生成目標代碼,鏈接到最終的應用程序之中。模板與宏相比,它站在更高的抽象層上面,宏操作的是字符串中的token,然而模板卻能夠操作C++中的類型。所以模板更加安全(因為有類型檢查),更加智能(可以根據(jù)上下文自動特化)……說完模板,來說說模板元編程。模板元編程其實就是復雜點兒的模板,簡單的模板在特化時基本只包含類型的查找與替換,這種模板可以看作是“類型安全的宏”。而模板元編程就是將一些通常編程時才有的概念比如:遞歸,分支等加入到模板特化過程中的模板,但其實說白了還是模板,自動代碼生成而已。普通用戶對 C++ 模板的使用可能不是很頻繁,大致限于泛型編程,但一些系統(tǒng)級的代碼,尤其是對通用性、性能要求極高的基礎庫(如 STL、Boost)幾乎不可避免的都大量地使用 C++ 模板,一個稍有規(guī)模的大量使用模板的程序,不可避免的要涉及元編程(如類型計算)。本文就是要剖析 C++ 模板元編程的機制。
C++ 模板是圖靈完備的,這使得 C++ 成為兩層次語言(two-level languages,中文暫且這么翻譯,文獻[9]),其中,執(zhí)行編譯計算的代碼稱為靜態(tài)代碼(static code),執(zhí)行運行期計算的代碼稱為動態(tài)代碼(dynamic code),C++ 的靜態(tài)代碼由模板實現(xiàn)(預處理的宏也算是能進行部分靜態(tài)計算吧,也就是能進行部分元編程,稱為宏元編程,見 Boost 元編程庫即 BCCL,具體來說 C++ 模板可以做以下事情:編譯期數(shù)值計算、類型計算、代碼計算(如循環(huán)展開),其中數(shù)值計算實際不太有意義,而類型計算和代碼計算可以使得代碼更加通用,更加易用,性能更好(但是也會讓代碼也更難閱讀,更難調(diào)試,有時也會有代碼膨脹問題)。總的來說模板元編程的優(yōu)勢在于:
1.以編譯耗時為代價換來卓越的運行期性能(一般用于為性能要求嚴格的數(shù)值計算換取更高的性能)。通常來說,一個有意義的程序的運行次數(shù)(或服役時間)總是遠遠超過編譯次數(shù)(或編譯時間)。
2.提供編譯期類型計算,通常這才是模板元編程大放異彩的地方。
模板元編程技術并非都是優(yōu)點:
1.代碼可讀性差,以類模板的方式描述算法也許有點抽象。
2.調(diào)試困難,元程序執(zhí)行于編譯期,沒有用于單步跟蹤元程序執(zhí)行的調(diào)試器(用于設置斷點、察看數(shù)據(jù)等)。程序員可做的只能是等待編譯過程失敗,然后人工破譯編譯器傾瀉到屏幕上的錯誤信息。
3.編譯時間長,通常帶有模板元程序的程序生成的代碼尺寸要比普通程序的大,
4.可移植性較差,對于模板元編程使用的高級模板特性,不同的編譯器的支持度不同。
編譯期計算在編譯過程中的位置請見下圖,可以看到關鍵是模板的機制在編譯具體代碼(模板實例)前執(zhí)行:

從編程范型(programming paradigm)上來說,C++ 模板是函數(shù)式編程(functional programming),它的主要特點是:函數(shù)調(diào)用不產(chǎn)生任何副作用(沒有可變的存儲),用遞歸形式實現(xiàn)循環(huán)結(jié)構(gòu)的功能。C++ 模板的特例化提供了條件判斷能力,而模板遞歸嵌套提供了循環(huán)的能力,這兩點使得其具有和普通語言一樣通用的能力(圖靈完備性)。從編程形式來看,模板的“<>”中的模板參數(shù)相當于函數(shù)調(diào)用的輸入?yún)?shù),模板中的 typedef 或 static const 或 enum 定義函數(shù)返回值(類型或數(shù)值,數(shù)值僅支持整型,如果需要可以通過編碼計算浮點數(shù)),代碼計算是通過類型計算進而選擇類型的函數(shù)實現(xiàn)的(C++ 屬于靜態(tài)類型語言,編譯器對類型的操控能力很強)。
示例:
#include <iostream>
template<typename T, int i = 1>
class CComputeSomething {
public:
typedef volatile T *retType; // 類型計算
enum {
retValume = i + CComputeSomething<T, i - 1>::retValume
}; // 數(shù)值計算,遞歸
static void f() {
std::cout << "CComputeSomething:i = " << i << " retValume = " << retValume << '\n';
}
};
//遞歸結(jié)束特例
template<typename T>
class CComputeSomething<T, 0> {
public:
enum {
retValume = 0
};
};
// 根據(jù)類型調(diào)用函數(shù),代碼計算
template<typename T>
class CComputingFunc {
public:
static void f() { T::f(); }
};
int main() {
CComputeSomething<int>::retType a = 0;
//這里的遞歸深度注意,不同編譯器允許的最大深度不同,編譯時添加 -ftemplate-depth=500來修改編譯器允許的遞歸最大深度
CComputingFunc<CComputeSomething<int, 500>>::f();
return 0;
}
C++ 模板元編程概覽框圖如下:

編譯期數(shù)值計算
第一個 C++ 模板元程序是 Erwin Unruh 在 1994 年寫的,這個程序計算小于給定數(shù) N 的全部素數(shù)(又叫質(zhì)數(shù)),程序并不運行(都不能通過編譯),而是讓編譯器在錯誤信息中顯示結(jié)果(直觀展現(xiàn)了是編譯期計算結(jié)果,C++ 模板元編程不是設計的功能,更像是在戲弄編譯器,當然 C++11 有所改變,下面以求和為例講解 C++ 模板編譯期數(shù)值計算的原理:
#include <iostream>
template<int N>
class Sumt {
public:
static const int ret = Sumt<N - 1>::ret + N;
};
template<>
class Sumt<0> {
public:
static const int ret = 0;
};
int main() {
std::cout << Sumt<5>::ret << '\n';
return 0;
}
當編譯器遇到 sumt<5> 時,試圖實例化之,sumt<5> 引用了 sumt<5-1> 即 sumt<4>,試圖實例化 sumt<4>,以此類推,直到 sumt<0>,sumt<0> 匹配模板特例,sumt<0>::ret 為 0,sumt<1>::ret 為 sumt<0>::ret+1 為 1,以此類推,sumt<5>::ret 為 15。值得一提的是,雖然對用戶來說程序只是輸出了一個編譯期常量 sumt<5>::ret,但在背后,編譯器其實至少處理了 sumt<0> 到 sumt<5> 共 6 個類型。
從這個例子我們也可以窺探 C++ 模板元編程的函數(shù)式編程范型,對比結(jié)構(gòu)化求和程序:for(i=0,sum=0; i<=N; ++i) sum+=i; 用逐步改變存儲(即變量 sum)的方式來對計算過程進行編程,模板元程序沒有可變的存儲(都是編譯期常量,是不可變的變量),要表達求和過程就要用很多個常量:sumt<0>::ret,sumt<1>::ret,…,sumt<5>::ret 。函數(shù)式編程看上去似乎效率低下(因為它和數(shù)學接近,而不是和硬件工作方式接近),但有自己的優(yōu)勢:描述問題更加簡潔清晰(前提是熟悉這種方式),沒有可變的變量就沒有數(shù)據(jù)依賴,方便進行并行化。
模板實現(xiàn)的條件 if 和 while :
template<bool c, typename Then, typename Else>
class IF_ {
};
template<typename Then, typename Else>
class IF_<true, Then, Else> {
public:
typedef Then reType;
};
template<typename Then, typename Else>
class IF_<false, Then, Else> {
public:
typedef Else reType;
};
// 隱含要求: Condition 返回值 ret,Statement 有類型 Next
template<template<typename> class Condition, typename Statement>
class WHILE_ {
template<typename Statement_>
class STOP {
public:
typedef Statement_ reType;
};
public:
typedef typename
IF_<Condition<Statement>::ret,
WHILE_<Condition, typename Statement::Next>,
STOP<Statement>>::reType::reType
reType;
};
模板循環(huán)展開
模板元編程實現(xiàn)的循環(huán)展開能夠達到和手動循環(huán)展開相近的性能(90% 以上),并且性能是循環(huán)版本的 2 倍多(如果扣除 memcpy 函數(shù)占據(jù)的部分加速比將更高,根據(jù) Amdahl 定律)。這里可能有人會想,既然循環(huán)次數(shù)固定,為什么不直接手動循環(huán)展開呢,難道就為了使用模板嗎?當然不是,有時候循環(huán)次數(shù)確實是編譯期固定值,但對用戶并不是固定的,比如要實現(xiàn)數(shù)學上向量計算的類,因為可能是 2、3、4 維,所以寫成模板,把維度作為 int 型模板參數(shù),這時因為不知道具體是幾維的也就不得不用循環(huán),不過因為維度信息在模板實例化時是編譯期常量且較小,所以編譯器很可能在代碼優(yōu)化時進行循環(huán)展開。
我們說過模板元編程實際上就是一些復雜的模板,雖然可以把一些復雜的運算提前到編譯器但是代碼閱讀性極差,如果你不是寫一些通用的大型的c++庫為了提高關鍵代碼的性能,千萬要適可而止,要不然止小心被打。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
虛函數(shù)被類的構(gòu)造析構(gòu)函數(shù)和成員函數(shù)調(diào)用虛函數(shù)的執(zhí)行過程
虛函數(shù)被類的構(gòu)造析構(gòu)函數(shù)和成員函數(shù)調(diào)用虛函數(shù)的執(zhí)行過程,需要的朋友可以參考下2013-02-02
C語言數(shù)據(jù)結(jié)構(gòu)不掛科指南之棧&隊列&數(shù)組詳解
自考重點、期末考試必過指南,這篇文章讓你理解什么是棧、什么是隊列、什么是數(shù)組。文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下2022-09-09
C++ STL 內(nèi) std::{bind/tuple/function} 簡單實現(xiàn)
這篇文章主要介紹了C++ STL 內(nèi) std::{bind/tuple/function} 簡單實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-02-02
一文帶你入木三分地理解字符串KMP算法以及C++實現(xiàn)
KMP算法是一種改進的字符串匹配算法,KMP算法的核心是利用匹配失敗后的信息,盡量減少模式串與主串的匹配次數(shù)以達到快速匹配的目的。本文就來和大家聊聊KMP算法的原理與實現(xiàn),需要的可以參考一下2022-12-12
Qt實戰(zhàn)案例之如何利用QProcess類實現(xiàn)啟動進程
這篇文章主要介紹了Qt實戰(zhàn)案例之如何利用QProcess類實現(xiàn)啟動進程,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-02-02

