C++ 高阶操作:模板元编程
星標(biāo)/置頂?公眾號(hào)👇,硬核文章第一時(shí)間送達(dá)!
鏈接 | https://www.cnblogs.com/xiangtingshen/p/11613183.html
泛型編程大家應(yīng)該都很熟悉了,主要就是利用模板實(shí)現(xiàn)“安全的宏”,而模板元編程區(qū)別于我們所知道的泛型編程,它是一種較為復(fù)雜的模板,屬于C++的高階操作了,它最主要的優(yōu)點(diǎn)就在于把計(jì)算過程提前到編譯期,能帶來可觀的性能提升。接下來,請和小編一起來學(xué)習(xí)吧。
1.概述
模板元編程(Template Meta programming,TMP)是編寫生成或操縱程序的程序,也是一種復(fù)雜且功能強(qiáng)大的編程范式(Programming Paradigm)。
C++模板給C++提供了元編程的能力,但大部分用戶對 C++ 模板的使用并不是很頻繁,大致限于泛型編程,在一些系統(tǒng)級(jí)的代碼,尤其是對通用性、性能要求極高的基礎(chǔ)庫(如 STL、Boost)幾乎不可避免在大量地使用 C++ 模板以及模板元編程。
模版元編程完全不同于普通的運(yùn)行期程序,因?yàn)槟0嬖绦虻膱?zhí)行完全是在編譯期,并且模版元程序操縱的數(shù)據(jù)不能是運(yùn)行時(shí)變量,只能是編譯期常量,不可修改。
另外它用到的語法元素也是相當(dāng)有限,不能使用運(yùn)行期的一些語法,比如if-else、for和while等語句都不能用。
因此,模版元編程需要很多技巧,常常需要類型重定義、枚舉常量、繼承、模板偏特化等方法來配合,因此模版元編程比較復(fù)雜也比較困難。
2.模板元編程的作用
C++ 模板最初是為實(shí)現(xiàn)泛型編程設(shè)計(jì)的,但人們發(fā)現(xiàn)模板的能力遠(yuǎn)遠(yuǎn)不止于那些設(shè)計(jì)的功能。
一個(gè)重要的理論結(jié)論就是:C++ 模板是圖靈完備的(Turing-complete),就是用 C++ 模板可以模擬圖靈機(jī)。
理論上說 C++ 模板可以執(zhí)行任何計(jì)算任務(wù),但實(shí)際上因?yàn)槟0迨蔷幾g期計(jì)算,其能力受到具體編譯器實(shí)現(xiàn)的限制(如遞歸嵌套深度,C++11 要求至少 1024,C++98 要求至少 17)。
C++ 模板元編程是“意外”功能,而不是設(shè)計(jì)的功能,這也是 C++ 模板元編程語法丑陋的根源。
C++ 模板是圖靈完備的,這使得 C++代碼存在兩層次,其中,執(zhí)行編譯計(jì)算的代碼稱為靜態(tài)代碼(static code),執(zhí)行運(yùn)行期計(jì)算的代碼稱為動(dòng)態(tài)代碼(dynamic code),C++的靜態(tài)代碼由模板實(shí)現(xiàn),編寫C++的靜態(tài)代碼,就是進(jìn)行C++的模板元編程。
具體來說 C++ 模板可以做以下事情:編譯期數(shù)值計(jì)算、類型計(jì)算、代碼計(jì)算(如循環(huán)展開),其中數(shù)值計(jì)算實(shí)際意義不大,而類型計(jì)算和代碼計(jì)算可以使得代碼更加通用,更加易用,性能更好(也更難閱讀,更難調(diào)試,有時(shí)也會(huì)有代碼膨脹問題)。
編譯期計(jì)算在編譯過程中的位置請見下圖。
使用模板元編程的基本原則就是:將負(fù)載由運(yùn)行時(shí)轉(zhuǎn)移到編譯時(shí),同時(shí)保持原有的抽象層次。
其中負(fù)載可以分為兩類,一類就是程序運(yùn)行本身的開銷,一類則是程序員需要編寫的代碼。
前者可以理解為編譯時(shí)優(yōu)化,后者則是為提高代碼復(fù)用度,從而提高程序員的編程效率。
圖靈完備:
圖靈完備是對計(jì)算能力的描述。
簡單判定圖靈完備的方法就是看該語言能否模擬出圖靈機(jī)圖靈不完備的語言常見原因有循環(huán)或遞歸受限(無法寫不終止的程序,如 while(true){}; ), 無法實(shí)現(xiàn)類似數(shù)組或列表這樣的數(shù)據(jù)結(jié)構(gòu)(不能模擬紙帶). 這會(huì)使能寫的程序有限圖靈完備可能帶來壞處, 如C++的模板語言, 模板語言是在類型檢查時(shí)執(zhí)行, 如果編譯器不加以檢查,我們完全可以寫出使得C++編譯器陷入死循環(huán)的程序.圖靈不完備也不是沒有意義, 有些場景我們需要限制語言本身. 如限制循環(huán)和遞歸, 可以保證該語言能寫的程序一定是終止的。
3.模板元編程的組成要素
從編程范式上來說,C++模板元編程是函數(shù)式編程,用遞歸形式實(shí)現(xiàn)循環(huán)結(jié)構(gòu)的功能,用C++ 模板的特例化提供了條件判斷能力,這兩點(diǎn)使得其具有和普通語言一樣通用的能力(圖靈完備性)。
模版元程序由元數(shù)據(jù)和元函數(shù)組成,元數(shù)據(jù)就是元編程可以操作的數(shù)據(jù),即C++編譯器在編譯期可以操作的數(shù)據(jù)。
元數(shù)據(jù)不是運(yùn)行期變量,只能是編譯期常量,不能修改,常見的元數(shù)據(jù)有enum枚舉常量、靜態(tài)常量、基本類型和自定義類型等。
元函數(shù)是模板元編程中用于操作處理元數(shù)據(jù)的“構(gòu)件”,可以在編譯期被“調(diào)用”,因?yàn)樗墓δ芎托问?和 運(yùn)行時(shí)的函數(shù)類似,而被稱為元函數(shù),它是元編程中最重要的構(gòu)件。
元函數(shù)實(shí)際上表現(xiàn)為C++的一個(gè)類、模板類或模板函數(shù),它的通常形式如下:
template<int N, int M> struct meta_func {static const int value = N+M; }調(diào)用元函數(shù)獲取value值:
cout<<meta_func<1, 2>::value<<endl;meta_func的執(zhí)行過程是在編譯期完成的,實(shí)際執(zhí)行程序時(shí),是沒有計(jì)算動(dòng)作而是直接使用編譯期的計(jì)算結(jié)果。元函數(shù)只處理元數(shù)據(jù),元數(shù)據(jù)是編譯期常量和類型,所以下面的代碼是編譯不過的:
int i = 1, j = 2; meta_func<i, j>::value; //錯(cuò)誤,元函數(shù)無法處理運(yùn)行時(shí)普通數(shù)據(jù)模板元編程產(chǎn)生的源程序是在編譯期執(zhí)行的程序,因此它首先要遵循C++和模板的語法,但是它操作的對象不是運(yùn)行時(shí)普通的變量,因此不能使用運(yùn)行時(shí)的C++關(guān)鍵字(如if、else、for),可用的語法元素相當(dāng)有限,最常用的是:
enum、static const //用來定義編譯期的整數(shù)常量; typedef/using //用于定義元數(shù)據(jù);[類型別名] T/Args... //聲明元數(shù)據(jù)類型;【模版參數(shù):類型形參,非類型形參】 Template //主要用于定義元函數(shù);【模版類,特化,偏特化】 :: //域運(yùn)算符,用于解析類型作用域獲取計(jì)算結(jié)果(元數(shù)據(jù))。【獲取元數(shù)據(jù),元類型】實(shí)際上,模板元中的if-else可以通過type_traits來實(shí)現(xiàn),它不僅僅可以在編譯期做判斷,還可以做計(jì)算、查詢、轉(zhuǎn)換和選擇。
模板元中的for等邏輯可以通過遞歸、重載、和模板特化(偏特化)等方法實(shí)現(xiàn)。
4.模板元編程的控制邏輯
第一個(gè) C++ 模板元程序由Erwin Unruh 在 1994 年編寫,這個(gè)程序計(jì)算小于給定數(shù) N 的全部素?cái)?shù)(又叫質(zhì)數(shù)),程序并不運(yùn)行(都不能通過編譯),而是讓編譯器在錯(cuò)誤信息中顯示結(jié)果(直觀展現(xiàn)了是編譯期計(jì)算結(jié)果,C++ 模板元編程不是設(shè)計(jì)的功能,更像是在戲弄編譯器。從此,C++模板元編程的能力開始被人們認(rèn)識(shí)到。
在模版元程序的具體實(shí)現(xiàn)時(shí),由于其執(zhí)行完全是在編譯期,所以不能使用運(yùn)行期的一些語法,比如if-else、for和while等語句都不能用。這些控制邏輯需要通過特殊的方法來實(shí)現(xiàn)。
4.1 if判斷
模板元編程中實(shí)現(xiàn)條件if判斷,參考如下代碼:
#include <iostream>template<bool c, typename Then, typename Else> class IF_ {}; //基礎(chǔ)類模版template<typename Then, typename Else> class IF_<true, Then, Else> { public: typedef Then reType; }; //類模版的偏特化; 如果第一個(gè)模版非類型參數(shù)為true,IF_<true, Then, Else>::reType的值為模版的第二個(gè)類型參數(shù)Thentemplate<typename Then, typename Else> class IF_<false,Then, Else> { public: typedef Else reType; }; //類模版的偏特化int main() {const int len = 4;// 定義一個(gè)指定字節(jié)數(shù)的類型typedefIF_<sizeof(short)==len, short,IF_<sizeof(int)==len, int,IF_<sizeof(long)==len, long,IF_<sizeof(long long)==len, long long,void>::reType>::reType>::reType>::reType int_my;std::cout << sizeof(int_my) << '\n'; }/*分析最里面的一層:* IF_<sizeof(long long)==len, long long, void>::reType* 如果sizeof(long long) == 4, 上面的表達(dá)式返回long long, 否則返回void */程序輸出結(jié)果:4。
實(shí)際上,從C++11開始,可以通過type_traits來實(shí)現(xiàn)。因?yàn)閠ype_traits提供了編譯期選擇特性:std::conditional,它在編譯期根據(jù)一個(gè)判斷式選擇兩個(gè)類型中的一個(gè),和條件表達(dá)式的語義類似,類似于一個(gè)三元表達(dá)式。它的原型是:
template< bool B, class T, class F > struct conditional;所以上面的代碼可以改寫為如下代碼:
程序同樣編譯輸出4。
4.2循環(huán)展開
編譯期的循環(huán)展開( Loop Unrolling)可以通過模板特化來結(jié)束遞歸展開,達(dá)到運(yùn)行期的for和while語句的功能。下面看一個(gè)編譯期數(shù)值計(jì)算的例子。
#include <iostream> template<int N> class sum {public: static const int ret = sum<N-1>::ret + N; }; template<> class sum<0> {public: static const int ret = 0; }; int main() {std::cout << sum<5>::ret <<std::endl;return 0; }程序輸出:15。
當(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。
值得一提的是,雖然對用戶來說程序只是輸出了一個(gè)編譯期常量sumt<5>::ret,但在背后,編譯器其實(shí)至少處理了sumt<0>到sumt<5>共6個(gè)類型。
從這個(gè)例子我們也可以窺探 C++ 模板元編程的函數(shù)式編程范型,對比結(jié)構(gòu)化求和程序:for(i=0,sum=0; i<=N; ++i) sum+=i;?用逐步改變存儲(chǔ)(即變量 sum)的方式來對計(jì)算過程進(jìn)行編程,模板元程序沒有可變的存儲(chǔ)(都是編譯期常量,是不可變的變量),要表達(dá)求和過程就要用很多個(gè)常量:sumt<0>::ret,sumt<1>::ret,...,sumt<5>::ret。
函數(shù)式編程看上去似乎效率低下(因?yàn)樗蛿?shù)學(xué)接近,而不是和硬件工作方式接近),但有自己的優(yōu)勢:描述問題更加簡潔清晰,沒有可變的變量就沒有數(shù)據(jù)依賴,方便進(jìn)行并行化。
4.3switch/case分支
同樣可以通過模板特化來模擬實(shí)現(xiàn)編譯期的switch/case分支功能。參考如下代碼:
#include <iostream> using namespace std; template<int v> class Case { public:static inline void Run() {cout << "default case" << endl;} }; template<> class Case<1> { public:static inline void Run() {cout << "case 1" << endl;} }; template<> class Case<2> { public:static inline void Run() {cout << "case 2" << endl;} }; int main() {Case<2>::Run(); }程序輸出結(jié)果:
5.特性、策略與標(biāo)簽
利用迭代器,我們可以實(shí)現(xiàn)很多通用算法,迭代器在容器與算法之間搭建了一座橋梁。求和函數(shù)模板如下:
#include <iostream> #include <vector> template<typename iter> typename iter::value_type mysum(iter begin, iter end) {typename iter::value_type sum(0);for(iter i=begin; i!=end; ++i)sum += *i;return sum; } int main() {std::vector<int> v;for(int i = 0; i<100; ++i)v.push_back(i);v.push_back(i);std::cout << mysum(v.begin(), v.end()) << '\n'; }程序編譯輸出:4950。
我們想讓 mysum() 對指針參數(shù)也能工作,畢竟迭代器就是模擬指針,但指針沒有嵌套類型 value_type,可以定義 mysum() 對指針類型的特例,但更好的辦法是在函數(shù)參數(shù)和 value_type 之間多加一層特性(traits)。
template<typename iter> class mytraits //標(biāo)準(zhǔn)容器通過這里獲取容器元素的類型 { public: typedef typename iter::value_type value_type; };template<typename T> class mytraits<T*> //數(shù)組類型的容器,通過這里獲取數(shù)組元素的類型 { public: typedef T value_type; };template<typename iter> typename mytraits<iter>::value_type mysum(iter begin, iter end) {typename mytraits<iter>::value_type sum(0);for(iter i=begin; i!=end; ++i)sum += *i;return sum; } int main() {int v[4] = {1,2,3,4};std::cout << mysum(v, v+4) << '\n';return 0; }程序輸出:10。
其實(shí),C++ 標(biāo)準(zhǔn)定義了類似的 traits, std::iterator_trait(另一個(gè)經(jīng)典例子是 std::numeric_limits) 。
traits特性對類型的信息(如 value_type、 reference)進(jìn)行包裝,使得上層代碼可以以統(tǒng)一的接口訪問這些信息。
C++ 模板元編程會(huì)涉及大量的類型計(jì)算,很多時(shí)候要提取類型的信息(typedef、 常量值等),如果這些類型信息的訪問方式不一致(如上面的迭代器和指針),我們將不得不定義特例,這會(huì)導(dǎo)致大量重復(fù)代碼的出現(xiàn)(另一種代碼膨脹),而通過加一層特性可以很好的解決這一問題。
另外,特性不僅可以對類型的信息進(jìn)行包裝,還可以提供更多信息,當(dāng)然,因?yàn)榧恿艘粚?#xff0c;也帶來復(fù)雜性。特性是一種提供元信息的手段。
策略(policy)一般是一個(gè)類模板,典型的策略是 STL 容器的分配器(如std::vector<>,完整聲明是template<class T, class Alloc=allocator<T>> class vector;)(這個(gè)參數(shù)有默認(rèn)參數(shù),即默認(rèn)存儲(chǔ)策略),策略類將模板的經(jīng)常變化的那一部分子功能塊集中起來作為模板參數(shù),這樣模板便可以更為通用,這和特性的思想是類似的。
標(biāo)簽(tag)一般是一個(gè)空類,其作用是作為一個(gè)獨(dú)一無二的類型名字用于標(biāo)記一些東西,典型的例子是 STL 迭代器的五種類型的名字。
input_iterator_tag output_iterator_tag forward_iterator_tag bidirectional_iterator_tag random_access_iterator_tag實(shí)際上,
std::vector<int>::iterator::iterator_category就是random_access_iterator_tag, 可以使用type_traits的特性is_same來判斷類型是否相同。
#include <iostream> #include <vector> #include <type_traits> int main() { std::cout << is_same<std::vector<int>::iterator::iterator_category, std::random_access_iterator_tag >::value << std::endl; return 0; }程序輸出:1。
有了這樣的判斷,還可以根據(jù)判斷結(jié)果做更復(fù)雜的元編程邏輯(如一個(gè)算法以迭代器為參數(shù),根據(jù)迭代器標(biāo)簽進(jìn)行特例化以對某種迭代器特殊處理)。標(biāo)簽還可以用來分辨函數(shù)重載。
6.小結(jié)
C++模板元編程是圖靈完備的且是函數(shù)式編程,主要特點(diǎn)是代碼在編譯期執(zhí)行,可用于編譯期數(shù)值計(jì)算,能夠獲得更有效率的運(yùn)行碼。模板的使用,也提高了代碼泛化。與此同時(shí),模板元編程也存一定缺點(diǎn),主要有:
(1)模板元編程產(chǎn)生的代碼較為復(fù)雜,難易閱讀,可讀性較差;
(2)大量模板的使用,編譯時(shí)容易導(dǎo)致代碼膨脹,提高了編譯時(shí)間;
(3)對于C++來說,由于各編譯器的差異,大量依賴模板元編程(特別是最新形式的)的代碼可能會(huì)有移植性的問題。
所以,對于模板元編程,我們需要揚(yáng)其長避其短,合理使用模板元編程。
往期推薦
??專輯 | 趣味設(shè)計(jì)模式
??專輯 | 音視頻開發(fā)
??專輯 | C++?進(jìn)階
??專輯 | 超硬核 Qt
??專輯 | 玩轉(zhuǎn) Linux
??專輯 | GitHub 開源推薦
??專輯 | 程序人生
關(guān)注公眾號(hào)「高效程序員」👇,一起優(yōu)秀!
回復(fù)?“入群”?進(jìn)技術(shù)交流群,回復(fù)?“1024”?獲取海量學(xué)習(xí)資源。
總結(jié)
以上是生活随笔為你收集整理的C++ 高阶操作:模板元编程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java面向对象游戏-黑熊怪变成武大郎吃
- 下一篇: 寒武纪笔试题——12小时制转24小时制(