我想知道上帝的構(gòu)思,其他的都祇是細(xì)節(jié)。 ——愛因斯坦
前言
C++的一些高級(jí)特性對(duì)于新人來(lái)說(shuō),很具有挑戰(zhàn)性,而模板就是其中之一,晦澀語(yǔ)法讓很多新人望而生畏;大多數(shù)人苦苦磨煉,卻始終沒有掌握這門絕學(xué),本文通過(guò)揭開模板的一些面紗,希望幫助新人掌握模板的心法,從而學(xué)會(huì)這門武功(技術(shù)),助你跨過(guò)C++這座大山,向C++頂級(jí)程序員邁進(jìn),升職加薪;
Content
C++模版的誕生
C++模板的實(shí)現(xiàn)
C++模版的誕生
程序 = 數(shù)據(jù)結(jié)構(gòu) + 算法
---Niklaus EmilWirth
程序本質(zhì)是數(shù)據(jù)結(jié)構(gòu)+算法,任何一門語(yǔ)言都可以這樣理解,這個(gè)公式對(duì)計(jì)算機(jī)科學(xué)的影響程度足以類似物理學(xué)中愛因斯坦的“E=MC^2”——一個(gè)公式展示出了程序的本質(zhì)。
最初C++是沒有標(biāo)準(zhǔn)庫(kù)的,任何一門語(yǔ)言的發(fā)展都需要標(biāo)準(zhǔn)庫(kù)的支持,為了讓C++更強(qiáng)大,更方便使用,Bjarne Stroustrup覺得需要給C++提供一個(gè)標(biāo)準(zhǔn)庫(kù),但標(biāo)準(zhǔn)庫(kù)設(shè)計(jì)需要一套統(tǒng)一機(jī)制來(lái)定義各種通用的容器(數(shù)據(jù)結(jié)構(gòu))和算法,并且能很好在一起配合,這就需要它們既要相對(duì)的獨(dú)立,又要操作接口保持統(tǒng)一,而且能夠很容易被別人使用(用到實(shí)際類中),同時(shí)又要保證開銷盡量?。ㄐ阅芤茫?。Bjarne Stroustrup 提議C++需要一種機(jī)制來(lái)解決這個(gè)問(wèn)題,所以就催生了模板的產(chǎn)生,最后經(jīng)標(biāo)準(zhǔn)委員會(huì)各路專家討論和發(fā)展,就發(fā)展成如今的模版, C++ 第一個(gè)正式的標(biāo)準(zhǔn)也加入了模板。
C++模版是一種解決方案,初心是提供參數(shù)化容器類和通用的算法(函數(shù)),目的就是為了減少重復(fù)代碼,讓通用性和高性能并存,提高C++程序員生產(chǎn)力。
什么是參數(shù)化容器類?
首先C++是可以提供OOP(面向?qū)ο螅┓妒骄幊痰恼Z(yǔ)言,所以支持類概念,類本身就是現(xiàn)實(shí)中一類事物的抽象,包括狀態(tài)和對(duì)應(yīng)的操作,打個(gè)比喻,大多數(shù)情況下我們談?wù)撈?,并不是指具體某輛汽車,而是某一類汽車(某個(gè)品牌),或者某一類車型的汽車。
所以我們?cè)O(shè)計(jì)汽車這個(gè)類的時(shí)候,各個(gè)汽車品牌的汽車大體框架(骨架)都差不多,都是4個(gè)輪子一個(gè)方向盤,而且操作基本上都是相同的,否則學(xué)車都要根據(jù)不同廠商汽車進(jìn)行學(xué)習(xí),所以我們可以用一個(gè)類來(lái)描述汽車的行為:
class Car{public:Car(...);//other operations...private:Tire m_tire[4];Wheel m_wheel;//other attributes...};
但這樣設(shè)計(jì)擴(kuò)展性不是很好,因?yàn)椴煌钠放频能?,可能方向盤形狀不一樣,輪胎外觀不一樣等等。所以要描述這些不同我們可能就會(huì)根據(jù)不同品牌去設(shè)計(jì)不同的類,這樣類就會(huì)變得很多,就會(huì)產(chǎn)生下面的問(wèn)題:
1. 代碼冗余,會(huì)產(chǎn)生視覺復(fù)雜性,本身相似的東西比較多;
2. 用戶很難通過(guò)配置去實(shí)現(xiàn)一輛車設(shè)計(jì),不好定制化一個(gè)汽車;
3. 如果有其中一個(gè)屬性有新的變化,就得實(shí)現(xiàn)一個(gè)新類,擴(kuò)展代價(jià)太大。
這個(gè)時(shí)候,就希望這個(gè)類是可以參數(shù)化的(屬性參數(shù)化),可以根據(jù)不同類型的參數(shù)進(jìn)行屬性配置,繼而生成不同的類。類模板就應(yīng)運(yùn)而生了,類模板就是用來(lái)實(shí)現(xiàn)參數(shù)化的容器類。
什么是通用算法?
程序=數(shù)據(jù)結(jié)構(gòu)+算法
算法就是對(duì)容器的操作,對(duì)數(shù)據(jù)結(jié)構(gòu)的操作,一般算法設(shè)計(jì)原則要滿足KISS原則,功能盡量單一,盡量通用,才能更好和不同容器配合,有些算法屬于控制類算法(比如遍歷),還需要和其他算法進(jìn)行配合,所以需要解決函數(shù)參數(shù)通用性問(wèn)題。舉個(gè)例子, 以前我們實(shí)現(xiàn)通用的排序函數(shù)可能是這樣:
void sort (void* first, void* last, Cmp cmp);
這樣的設(shè)計(jì)有下面一些問(wèn)題:
1. 為了支持多種類型,需要采用void*參數(shù),但是void*參數(shù)是一種類型不安全參數(shù),在運(yùn)行時(shí)候需要通過(guò)類型轉(zhuǎn)換來(lái)訪問(wèn)數(shù)據(jù)。
2. 因?yàn)榫幾g器不知道數(shù)據(jù)類型,那些對(duì)void*指針進(jìn)行偏移操作(算術(shù)操作)會(huì)非常危險(xiǎn)(GNU支持),所以操作會(huì)特別小心,這個(gè)給實(shí)現(xiàn)增加了復(fù)雜度。
所以要滿足通用(支持各種容器),設(shè)計(jì)復(fù)雜度低,效率高,類型安全的算法,模板函數(shù)就應(yīng)運(yùn)而生了,模板函數(shù)就是用來(lái)實(shí)現(xiàn)通用算法并滿足上面要求。
C++模板的實(shí)現(xiàn)
C++標(biāo)準(zhǔn)委員會(huì)采用一套類似函數(shù)式語(yǔ)言的語(yǔ)法來(lái)設(shè)計(jì)C++模板,而且設(shè)計(jì)成圖靈完備 (Turing-complete)(詳見參考),我們可以把C++模板看成是一種新的語(yǔ)言,而且可以看成是函數(shù)式編程語(yǔ)言,只是設(shè)計(jì)依附在(借助于)C++其他基礎(chǔ)語(yǔ)法上(類和函數(shù))。
C++實(shí)現(xiàn)類模板(class template)技術(shù)
1.定義模板類,讓每個(gè)模板類擁有模板簽名。
templateclass X{...};
上面的模板簽名可以理解成:X; 主要包括模板參數(shù)和模板名字X(類名), 基本的語(yǔ)法可以參考《C++ Templates: The Complete Guide》,《C++ primer》等書籍。
模板參數(shù)在形式上主要包括四類,為什么會(huì)存在這些分類,主要是滿足不同類對(duì)參數(shù)化的需求:
-
type template parameter: 類型模板參數(shù),以class或typename 標(biāo)記;此類主要是解決樸實(shí)的參數(shù)化類的問(wèn)題(上面描述的問(wèn)題),也是模板設(shè)計(jì)的初衷。
-
non-type template parameter: 非類型模板參數(shù),比如整型,布爾,枚舉,指針,引用等;此類主要是提供給大小,長(zhǎng)度等整型標(biāo)量參數(shù)的控制,其次還提供參數(shù)算術(shù)運(yùn)算能力,這些能力結(jié)合模板特化為模板提供了初始化值,條件判斷,遞歸循環(huán)等能力,這些能力促使模板擁有圖靈完備的計(jì)算能力。
-
template template parameter,模板參數(shù)是模板,此類參數(shù)需要依賴其他模板參數(shù)(作為自己的入?yún)ⅲ?,然后生成新的模板參?shù),可以用于策略類的設(shè)計(jì)policy-base class。
-
parameter pack,C++11的變長(zhǎng)模板參數(shù),此類參數(shù)是C++11新增的,主要的目的是支持模板參數(shù)個(gè)數(shù)的動(dòng)態(tài)變化,類似函數(shù)的變參,但有自己獨(dú)有語(yǔ)法用于定義和解析(unpack),模板變參主要用于支持參數(shù)個(gè)數(shù)變化的類和函數(shù),比如std::bind,可以綁定不同函數(shù)和對(duì)應(yīng)參數(shù),惰性執(zhí)行,模板變參結(jié)合std::tuple就可以實(shí)現(xiàn)。
2. 在用模板類聲明變量的地方,把模板實(shí)參(Arguments)(類型)帶入模板類,然后按照匹配規(guī)則進(jìn)行匹配,選擇最佳匹配模板. 模板實(shí)參和形參類似于函數(shù)的形參和實(shí)參,模板實(shí)參只能是在編譯時(shí)期確定的類型或者常量,C++17支持模板類實(shí)參推導(dǎo)。
3. 選好模板類之后,編譯器會(huì)進(jìn)行模板類實(shí)例化--記帶入實(shí)際參數(shù)的類型或者常量自動(dòng)生成代碼,然后再進(jìn)行通常的編譯。
C++實(shí)現(xiàn)模板函數(shù)(function template)技術(shù)
模板函數(shù)實(shí)現(xiàn)技術(shù)和模板類形式上差不多:
templateretType function_name(T t);
其中幾個(gè)關(guān)鍵點(diǎn):
-
函數(shù)模板的簽名包括模板參數(shù),返回值,函數(shù)名,函數(shù)參數(shù), cv-qualifier;
-
函數(shù)模板編譯順序大致:名稱查找(可能涉及參數(shù)依賴查找)->實(shí)參推導(dǎo)->模板實(shí)參替換(實(shí)例化,可能涉及 SFINAE)->函數(shù)重載決議->編譯;
-
函數(shù)模板可以在實(shí)例化時(shí)候進(jìn)行參數(shù)推導(dǎo),必須知道每個(gè)模板的實(shí)參,但不必指定每個(gè)模板的實(shí)參。編譯器會(huì)從函數(shù)實(shí)參推導(dǎo)缺失的模板實(shí)參。這發(fā)生在嘗試調(diào)用函數(shù)、取函數(shù)模板地址時(shí),和某些其他語(yǔ)境中;
-
函數(shù)模板在進(jìn)行實(shí)例化后會(huì)進(jìn)行函數(shù)重載解析, 此時(shí)的函數(shù)簽名不包括返回值(template argument deduction/substitution);
-
函數(shù)模板實(shí)例化過(guò)程中,參數(shù)推導(dǎo)不匹配所有的模板或者同時(shí)存在多個(gè)模板實(shí)例滿足,或者函數(shù)重載決議有歧義等,實(shí)例化失??;
-
為了編譯函數(shù)模板調(diào)用,編譯器必須在非模板重載、模板重載和模板重載的特化間決定一個(gè)無(wú)歧義最佳的模板;
C++模板的核心技術(shù)
1. SFINAE -Substitution failure is not an error
要理解這句話的關(guān)鍵點(diǎn)是failure和error在模板實(shí)例化中意義,模板實(shí)例化時(shí)候,編譯器會(huì)用模板實(shí)參或者通過(guò)模板實(shí)參推導(dǎo)出參數(shù)類型帶入可能的模板集(模板備選集合)中一個(gè)一個(gè)匹配,找到最優(yōu)匹配的模板定義,
Failure:在模板集中,單個(gè)匹配失??;
Error:在模板集中,所有的匹配失??;
所以單個(gè)匹配失敗,不能報(bào)錯(cuò)誤,只有所有的匹配都失敗了才報(bào)錯(cuò)誤。
2. 模板特化
模板特化為了支持模板類或者模板函數(shù)在特定的情況(指明模板的部分參數(shù)(偏特化)或者全部參數(shù)(完全特化))下特殊實(shí)現(xiàn)和優(yōu)化,而這個(gè)機(jī)制給與模板某些高階功能提供了基礎(chǔ),比如模板的遞歸(提供遞歸終止條件實(shí)現(xiàn)),模板條件判斷(提供true或者false 條件實(shí)現(xiàn))等。
3. 模板實(shí)參推導(dǎo)
模板實(shí)參推導(dǎo)機(jī)制給與編譯器可以通過(guò)實(shí)參去反推模板的形參,然后對(duì)模板進(jìn)行實(shí)例化,具體推導(dǎo)規(guī)則見參考;
4. 模板計(jì)算
模板參數(shù)支持兩大類計(jì)算:
一類是類型計(jì)算(通過(guò)不同的模板參數(shù)返回不同的類型),此類計(jì)算為構(gòu)建類型系統(tǒng)提供了基礎(chǔ),也是泛型編程的基礎(chǔ);
一類是整型參數(shù)的算術(shù)運(yùn)算, 此類計(jì)算提供了模板在實(shí)例化時(shí)候動(dòng)態(tài)匹配模板的能力;實(shí)參通過(guò)計(jì)算后的結(jié)果作為新的實(shí)參去匹配特定模板(模板特化)。
5. 模板遞歸
模板遞歸是模板元編程的基礎(chǔ),也是C++11變參模板的基礎(chǔ)。
C++模版的應(yīng)用場(chǎng)景
1. C++ Library:
可以實(shí)現(xiàn)通用的容器(Containers)和算法(Algorithms),比如STL,Boost等,使用模板技術(shù)實(shí)現(xiàn)的迭代器(Iterators)和仿函數(shù)(Functors)可以很好讓容器和算法可以自由搭配和更好的配合;
2. C++ type traits
通過(guò)模板技術(shù),C++ type traits實(shí)現(xiàn)了一套操作類型特性的系統(tǒng),C++是靜態(tài)類型語(yǔ)言,在編譯時(shí)候需要對(duì)變量和函數(shù)進(jìn)行類型檢查,這個(gè)時(shí)候type traits可以提供更多類型信息給編譯器, 能讓程序做出更多策略選擇和特定類型的深度優(yōu)化,Type Traits有助于編寫通用、可復(fù)用的代碼。
C++創(chuàng)始人對(duì)traits的理解:
"Think of a trait as a small object whose main purpose is to carry information used by another object or algorithm to determine "policy" or "implementation details". - Bjarne Stroustrup
而這個(gè)技術(shù),在其他語(yǔ)言也有類似實(shí)現(xiàn),比如go的interface,java的注解,反射機(jī)制等。
3. Template metaprogramming-TMP
隨著模板技術(shù)發(fā)展,模板元編程逐漸被人們發(fā)掘出來(lái),metaprogramming本意是進(jìn)行源代碼生成的編程(代碼生成器),同時(shí)也是對(duì)編程本身的一種更高級(jí)的抽象,好比我們?cè)J(rèn)知這些概念,就是對(duì)學(xué)習(xí)本身更高級(jí)的抽象。TMP通過(guò)模板實(shí)現(xiàn)一套“新的語(yǔ)言”(條件,遞歸,初始化,變量等),由于模板是圖靈完備,理論上可以實(shí)現(xiàn)任何可計(jì)算編程,把本來(lái)在運(yùn)行期實(shí)現(xiàn)部分功能可以移到編譯期實(shí)現(xiàn),節(jié)省運(yùn)行時(shí)開銷,比如進(jìn)行循環(huán)展開,量綱分析等。
4. Policy-Based Class Design
C++ Policy class design 首見于 Andrei Alexandrescu 出版的 《Modern C++ Design》一書以及他在C/C++ Users Journal雜志專欄 Generic,參考wiki。通過(guò)把不同策略設(shè)計(jì)成獨(dú)立的類,然后通過(guò)模板參數(shù)對(duì)主類進(jìn)行配置,通常policy-base class design采用繼承方式去實(shí)現(xiàn),這要求每個(gè)策略在設(shè)計(jì)的時(shí)候要相互獨(dú)立正交。STL還結(jié)合CRTP (Curiously recurring template pattern)等模板技術(shù),實(shí)現(xiàn)類似動(dòng)態(tài)多態(tài)(虛函數(shù))的靜態(tài)多態(tài),減少運(yùn)行開銷。
5. Generic Programming(泛型編程)
由于模板這種對(duì)類型強(qiáng)有力的抽象能力,能讓容器和算法更加通用,這一系列的編程手法,慢慢引申出一種新的編程范式:泛型編程。泛型編程是對(duì)類型的抽象接口進(jìn)行編程,STL庫(kù)就是泛型編程經(jīng)典范例。
C++模版的展望
1. 模版的代價(jià)
沒有任何事物是完美的,模板設(shè)計(jì)如此精良也有代價(jià)的,模板的代碼和通常的代碼比起來(lái),
-
代碼可讀性差,理解門檻高
一般人初學(xué)者很難看懂,開發(fā)和調(diào)試比較麻煩,對(duì)人員要求高,是跨越C++三座大山之一;
-
代碼實(shí)現(xiàn)穩(wěn)定性代價(jià)大
對(duì)模板代碼,實(shí)際上很難覆蓋所有的測(cè)試,為了保證代碼的健壯性,需要大量高質(zhì)量的測(cè)試,各個(gè)平臺(tái)(編譯器)支持力度也不一樣(比如模板遞歸深度,模板特性等),可移植性不能完全保證。模板多個(gè)實(shí)例很有可能會(huì)隱式地增加二進(jìn)制文件的大小等,所以模板在某些情況下有一定代價(jià),一定要在擅長(zhǎng)的地方發(fā)揮才能;
如何降低門檻,對(duì)初學(xué)者更友好,如何降低復(fù)雜性,這個(gè)是C++未來(lái)發(fā)展重要的方向?,F(xiàn)代c++正在追求讓模板,或者說(shuō)編譯期的計(jì)算和泛型約束變簡(jiǎn)單,constexpr,concept,fold expression,還有C++ 20一大堆consteval,constinit,constexpr virtual function,constexpr dynamic cast,constexpr container等等特性的加入就是為了解決這些問(wèn)題。曾經(jīng)的遞歸變成了普通的constexpr函數(shù),曾經(jīng)的SFINAE變成了concept,曾經(jīng)的枚舉常量變成了constexpr常量,曾經(jīng)的遞歸展開變成了fold expression,越來(lái)越簡(jiǎn)單,友好了。
2. 基于模板的設(shè)計(jì)模式
隨著C++模板技術(shù)的發(fā)展,以及大量實(shí)戰(zhàn)的經(jīng)驗(yàn)總結(jié),逐漸形成了一些基于模板的經(jīng)典設(shè)計(jì),比如STL里面的特性(traits),策略(policy),標(biāo)簽(tag)等技法;Boost.MPL庫(kù)(高級(jí)C++模板元編程框架)設(shè)計(jì);Andrei Alexandrescu 提出的Policy-Based Class Design;以及Jim Coplien的curiously recurring template pattern (CRTP),以及衍生Mixin技法;或許未來(lái),基于模板可以衍生更多的設(shè)計(jì)模式,而這些優(yōu)秀的設(shè)計(jì)模式可以實(shí)現(xiàn)最大性能和零成本抽象,這個(gè)也是C++的核心精神。
3. 模板的未來(lái)
隨著模板衍生出來(lái)的泛型編程,模板元編程,模板函數(shù)式編程等理念的發(fā)展,將來(lái)也許會(huì)發(fā)展出更抽象,更通用編程理念。模板本身是圖靈完備的,所以可以結(jié)合C++其他特性,編譯期常量和常量表達(dá)式,編譯期計(jì)算,繼承,友元friend等開闊出更多優(yōu)雅的設(shè)計(jì),比如元容器,類型擦除,自省和反射(靜態(tài)反射和metaclass)等,將來(lái)會(huì)出現(xiàn)更多優(yōu)秀的設(shè)計(jì)。
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!