淺談C++模板元編程
所謂元編程就是編寫(xiě)直接生成或操縱程序的程序,C++ 模板給 C++ 語(yǔ)言提供了元編程的能力,模板使 C++ 編程變得異常靈活,能實(shí)現(xiàn)很多高級(jí)動(dòng)態(tài)語(yǔ)言才有的特性(語(yǔ)法上可能比較丑陋,一些歷史原因見(jiàn)下文)。模板元編程的根在模板。模板的使命很簡(jiǎn)單:為自動(dòng)代碼生成提供方便。提高程序員生產(chǎn)率的一個(gè)非常有效的方法就是“代碼復(fù)用”,而面向?qū)ο蠛苤匾囊粋€(gè)貢獻(xiàn)就是通過(guò)內(nèi)部緊耦合和外部松耦合將“思想”轉(zhuǎn)化成一個(gè)一個(gè)容易復(fù)用的“概念”。但是面向?qū)ο筇峁┑墓ぞ呦淅锩嫠睦^承,組合與多態(tài)并不能完全滿足實(shí)際編程中對(duì)于代碼復(fù)用的全部要求,于是模板就應(yīng)運(yùn)而生了。
模板是更智能的宏。模板和宏都是編譯前代碼生成,像宏一樣,模板代碼會(huì)被編譯器在編譯的第一階段(在內(nèi)部轉(zhuǎn),這點(diǎn)兒與預(yù)編譯器不同)就展開(kāi)成合法的C++代碼,然后根據(jù)展開(kāi)的代碼生成目標(biāo)代碼,鏈接到最終的應(yīng)用程序之中。模板與宏相比,它站在更高的抽象層上面,宏操作的是字符串中的token,然而模板卻能夠操作C++中的類型。所以模板更加安全(因?yàn)橛蓄愋蜋z查),更加智能(可以根據(jù)上下文自動(dòng)特化)……說(shuō)完模板,來(lái)說(shuō)說(shuō)模板元編程。模板元編程其實(shí)就是復(fù)雜點(diǎn)兒的模板,簡(jiǎn)單的模板在特化時(shí)基本只包含類型的查找與替換,這種模板可以看作是“類型安全的宏”。而模板元編程就是將一些通常編程時(shí)才有的概念比如:遞歸,分支等加入到模板特化過(guò)程中的模板,但其實(shí)說(shuō)白了還是模板,自動(dòng)代碼生成而已。普通用戶對(duì) C++ 模板的使用可能不是很頻繁,大致限于泛型編程,但一些系統(tǒng)級(jí)的代碼,尤其是對(duì)通用性、性能要求極高的基礎(chǔ)庫(kù)(如 STL、Boost)幾乎不可避免的都大量地使用 C++ 模板,一個(gè)稍有規(guī)模的大量使用模板的程序,不可避免的要涉及元編程(如類型計(jì)算)。本文就是要剖析 C++ 模板元編程的機(jī)制。
C++ 模板是圖靈完備的,這使得 C++ 成為兩層次語(yǔ)言(two-level languages,中文暫且這么翻譯,文獻(xiàn)[9]),其中,執(zhí)行編譯計(jì)算的代碼稱為靜態(tài)代碼(static code),執(zhí)行運(yùn)行期計(jì)算的代碼稱為動(dòng)態(tài)代碼(dynamic code),C++ 的靜態(tài)代碼由模板實(shí)現(xiàn)(預(yù)處理的宏也算是能進(jìn)行部分靜態(tài)計(jì)算吧,也就是能進(jìn)行部分元編程,稱為宏元編程,見(jiàn) Boost 元編程庫(kù)即 BCCL,具體來(lái)說(shuō) C++ 模板可以做以下事情:編譯期數(shù)值計(jì)算、類型計(jì)算、代碼計(jì)算(如循環(huán)展開(kāi)),其中數(shù)值計(jì)算實(shí)際不太有意義,而類型計(jì)算和代碼計(jì)算可以使得代碼更加通用,更加易用,性能更好(但是也會(huì)讓代碼也更難閱讀,更難調(diào)試,有時(shí)也會(huì)有代碼膨脹問(wèn)題)。總的來(lái)說(shuō)模板元編程的優(yōu)勢(shì)在于:
1.以編譯耗時(shí)為代價(jià)換來(lái)卓越的運(yùn)行期性能(一般用于為性能要求嚴(yán)格的數(shù)值計(jì)算換取更高的性能)。通常來(lái)說(shuō),一個(gè)有意義的程序的運(yùn)行次數(shù)(或服役時(shí)間)總是遠(yuǎn)遠(yuǎn)超過(guò)編譯次數(shù)(或編譯時(shí)間)。
2.提供編譯期類型計(jì)算,通常這才是模板元編程大放異彩的地方。
模板元編程技術(shù)并非都是優(yōu)點(diǎn):
1.代碼可讀性差,以類模板的方式描述算法也許有點(diǎn)抽象。
2.調(diào)試?yán)щy,元程序執(zhí)行于編譯期,沒(méi)有用于單步跟蹤元程序執(zhí)行的調(diào)試器(用于設(shè)置斷點(diǎn)、察看數(shù)據(jù)等)。程序員可做的只能是等待編譯過(guò)程失敗,然后人工破譯編譯器傾瀉到屏幕上的錯(cuò)誤信息。
3.編譯時(shí)間長(zhǎng),通常帶有模板元程序的程序生成的代碼尺寸要比普通程序的大,
4.可移植性較差,對(duì)于模板元編程使用的高級(jí)模板特性,不同的編譯器的支持度不同。
編譯期計(jì)算在編譯過(guò)程中的位置請(qǐng)見(jiàn)下圖,可以看到關(guān)鍵是模板的機(jī)制在編譯具體代碼(模板實(shí)例)前執(zhí)行:
從編程范型(programming paradigm)上來(lái)說(shuō),C++ 模板是函數(shù)式編程(functional programming),它的主要特點(diǎn)是:函數(shù)調(diào)用不產(chǎn)生任何副作用(沒(méi)有可變的存儲(chǔ)),用遞歸形式實(shí)現(xiàn)循環(huán)結(jié)構(gòu)的功能。C++ 模板的特例化提供了條件判斷能力,而模板遞歸嵌套提供了循環(huán)的能力,這兩點(diǎn)使得其具有和普通語(yǔ)言一樣通用的能力(圖靈完備性)。從編程形式來(lái)看,模板的“<>”中的模板參數(shù)相當(dāng)于函數(shù)調(diào)用的輸入?yún)?shù),模板中的 typedef 或 static const 或 enum 定義函數(shù)返回值(類型或數(shù)值,數(shù)值僅支持整型,如果需要可以通過(guò)編碼計(jì)算浮點(diǎn)數(shù)),代碼計(jì)算是通過(guò)類型計(jì)算進(jìn)而選擇類型的函數(shù)實(shí)現(xiàn)的(C++ 屬于靜態(tài)類型語(yǔ)言,編譯器對(duì)類型的操控能力很強(qiáng))。
示例:
#include <iostream> template<typename T, int i = 1> class CComputeSomething { public: typedef volatile T *retType; // 類型計(jì)算 enum { retValume = i + CComputeSomething<T, i - 1>::retValume }; // 數(shù)值計(jì)算,遞歸 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ù),代碼計(jì)算 template<typename T> class CComputingFunc { public: static void f() { T::f(); } }; int main() { CComputeSomething<int>::retType a = 0; //這里的遞歸深度注意,不同編譯器允許的最大深度不同,編譯時(shí)添加 -ftemplate-depth=500來(lái)修改編譯器允許的遞歸最大深度 CComputingFunc<CComputeSomething<int, 500>>::f(); return 0; }
C++ 模板元編程概覽框圖如下:
編譯期數(shù)值計(jì)算
第一個(gè) C++ 模板元程序是 Erwin Unruh 在 1994 年寫(xiě)的,這個(gè)程序計(jì)算小于給定數(shù) N 的全部素?cái)?shù)(又叫質(zhì)數(shù)),程序并不運(yùn)行(都不能通過(guò)編譯),而是讓編譯器在錯(cuò)誤信息中顯示結(jié)果(直觀展現(xiàn)了是編譯期計(jì)算結(jié)果,C++ 模板元編程不是設(shè)計(jì)的功能,更像是在戲弄編譯器,當(dāng)然 C++11 有所改變,下面以求和為例講解 C++ 模板編譯期數(shù)值計(jì)算的原理:
#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; }
當(dāng)編譯器遇到 sumt<5> 時(shí),試圖實(shí)例化之,sumt<5> 引用了 sumt<5-1> 即 sumt<4>,試圖實(shí)例化 sumt<4>,以此類推,直到 sumt<0>,sumt<0> 匹配模板特例,sumt<0>::ret 為 0,sumt<1>::ret 為 sumt<0>::ret+1 為 1,以此類推,sumt<5>::ret 為 15。值得一提的是,雖然對(duì)用戶來(lái)說(shuō)程序只是輸出了一個(gè)編譯期常量 sumt<5>::ret,但在背后,編譯器其實(shí)至少處理了 sumt<0> 到 sumt<5> 共 6 個(gè)類型。
從這個(gè)例子我們也可以窺探 C++ 模板元編程的函數(shù)式編程范型,對(duì)比結(jié)構(gòu)化求和程序:for(i=0,sum=0; i<=N; ++i) sum+=i; 用逐步改變存儲(chǔ)(即變量 sum)的方式來(lái)對(duì)計(jì)算過(guò)程進(jìn)行編程,模板元程序沒(méi)有可變的存儲(chǔ)(都是編譯期常量,是不可變的變量),要表達(dá)求和過(guò)程就要用很多個(gè)常量:sumt<0>::ret,sumt<1>::ret,…,sumt<5>::ret 。函數(shù)式編程看上去似乎效率低下(因?yàn)樗蛿?shù)學(xué)接近,而不是和硬件工作方式接近),但有自己的優(yōu)勢(shì):描述問(wèn)題更加簡(jiǎn)潔清晰(前提是熟悉這種方式),沒(méi)有可變的變量就沒(méi)有數(shù)據(jù)依賴,方便進(jìn)行并行化。
模板實(shí)現(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)展開(kāi)
模板元編程實(shí)現(xiàn)的循環(huán)展開(kāi)能夠達(dá)到和手動(dòng)循環(huán)展開(kāi)相近的性能(90% 以上),并且性能是循環(huán)版本的 2 倍多(如果扣除 memcpy 函數(shù)占據(jù)的部分加速比將更高,根據(jù) Amdahl 定律)。這里可能有人會(huì)想,既然循環(huán)次數(shù)固定,為什么不直接手動(dòng)循環(huán)展開(kāi)呢,難道就為了使用模板嗎?當(dāng)然不是,有時(shí)候循環(huán)次數(shù)確實(shí)是編譯期固定值,但對(duì)用戶并不是固定的,比如要實(shí)現(xiàn)數(shù)學(xué)上向量計(jì)算的類,因?yàn)榭赡苁?2、3、4 維,所以寫(xiě)成模板,把維度作為 int 型模板參數(shù),這時(shí)因?yàn)椴恢谰唧w是幾維的也就不得不用循環(huán),不過(guò)因?yàn)榫S度信息在模板實(shí)例化時(shí)是編譯期常量且較小,所以編譯器很可能在代碼優(yōu)化時(shí)進(jìn)行循環(huán)展開(kāi)。
我們說(shuō)過(guò)模板元編程實(shí)際上就是一些復(fù)雜的模板,雖然可以把一些復(fù)雜的運(yùn)算提前到編譯器但是代碼閱讀性極差,如果你不是寫(xiě)一些通用的大型的c++庫(kù)為了提高關(guān)鍵代碼的性能,千萬(wàn)要適可而止,要不然止小心被打。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
C++基于Directx MMX實(shí)現(xiàn)的圖像灰度轉(zhuǎn)換代碼
這篇文章主要介紹了C++基于Directx MMX實(shí)現(xiàn)的圖像灰度轉(zhuǎn)換代碼,需要的朋友可以參考下2014-08-08緩存處理函數(shù)storageKeySuffix操作示例解析
這篇文章主要介紹了淺析緩存處理函數(shù)storageKeySuffix操作示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08詳解C++調(diào)用Python腳本中的函數(shù)的實(shí)例代碼
這篇文章主要介紹了C++調(diào)用Python腳本中的函數(shù) ,需要的朋友可以參考下2018-11-11C++設(shè)計(jì)一個(gè)簡(jiǎn)單內(nèi)存池的全過(guò)程
利用C/C++開(kāi)發(fā)大型應(yīng)用程序中,內(nèi)存的管理與分配是一個(gè)需要認(rèn)真考慮的部分,下面這篇文章主要給大家介紹了關(guān)于C++設(shè)計(jì)一個(gè)簡(jiǎn)單內(nèi)存池的全過(guò)程,需要的朋友可以參考下2021-09-09C++中的按位與&、按位與或|、按位異或^運(yùn)算符詳解
這篇文章主要介紹了C++中的按位與&、按位與或|、按位異或^運(yùn)算符,是C++入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2016-01-01C語(yǔ)言輸出旋轉(zhuǎn)后數(shù)組中的最小數(shù)元素的算法原理與實(shí)例
這篇文章主要介紹了C語(yǔ)言輸出旋轉(zhuǎn)后數(shù)組中的最小數(shù)元素的算法原理與實(shí)例,數(shù)組旋轉(zhuǎn)就是把開(kāi)頭的幾個(gè)指定的元素放到數(shù)組的末尾,需要的朋友可以參考下2016-03-03