C++模板元編程實(shí)戰(zhàn)_第1頁
C++模板元編程實(shí)戰(zhàn)_第2頁
C++模板元編程實(shí)戰(zhàn)_第3頁
C++模板元編程實(shí)戰(zhàn)_第4頁
C++模板元編程實(shí)戰(zhàn)_第5頁
已閱讀5頁,還剩106頁未讀 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)

文檔簡介

C++模板元編程實(shí)戰(zhàn)注:因內(nèi)容過長上傳受限制,本文檔只顯示部分內(nèi)容,完整版文檔請下載此文檔后留言謝謝。目錄\h第一部分元編程基礎(chǔ)技術(shù)\h第1章基本技巧\h1.1元函數(shù)與type_traits\h1.1.1元函數(shù)介紹\h1.1.2類型元函數(shù)\h1.1.3各式各樣的元函數(shù)\h1.1.4type_traits\h1.1.5元函數(shù)與宏\h1.1.6本文中元函數(shù)的命名方式\h1.2模板型模板參數(shù)與容器模板\h1.2.1模板作為元函數(shù)的輸入\h1.2.2模板作為元函數(shù)的輸出\h1.2.3容器模板\h1.3順序、分支與循環(huán)代碼的編寫\h1.3.1順序執(zhí)行的代碼\h1.3.2分支執(zhí)行的代碼\h1.3.3循環(huán)執(zhí)行的代碼\h1.3.4小心:實(shí)例化爆炸與編譯崩潰\h1.3.5分支選擇與短路邏輯\h1.4奇特的遞歸模板式\h1.5小結(jié)\h1.6練習(xí)\h第2章異類詞典與policy模板\h2.1具名參數(shù)簡介\h2.2異類詞典\h2.2.1模塊的使用方式\h2.2.2鍵的表示\h2.2.3異類詞典的實(shí)現(xiàn)\h2.2.4VarTypeDict的性能簡析\h2.2.5用std::tuple作為緩存\h2.3policy模板\h2.3.1policy介紹\h2.3.2定義policy與policy對象(模板)\h2.3.3使用policy\h2.3.4背景知識:支配與虛繼承\(zhòng)h2.3.5policy對象與policy支配結(jié)構(gòu)\h2.3.6policy選擇元函數(shù)\h2.3.7使用宏簡化policy對象的聲明\h2.4小結(jié)\h2.5練習(xí)\h第3章深度學(xué)習(xí)概述\h3.1深度學(xué)習(xí)簡介\h3.1.1從機(jī)器學(xué)習(xí)到深度學(xué)習(xí)\h3.1.2各式各樣的人工神經(jīng)網(wǎng)絡(luò)\h3.1.3深度學(xué)習(xí)系統(tǒng)的組織與訓(xùn)練\h3.2本文所實(shí)現(xiàn)的框架:MetaNN\h3.2.1從矩陣計算工具到深度學(xué)習(xí)框架\h3.2.2MetaNN介紹\h3.2.3本文將要討論的內(nèi)容\h3.2.4本文不會涉及的主題\h3.3小結(jié)\h第4章類型體系與基本數(shù)據(jù)類型\h4.1類型體系\h4.1.1類型體系介紹\h4.1.2迭代器分類體系\h4.1.3將標(biāo)簽作為模板參數(shù)\h4.1.4MetaNN的類型體系\h4.1.5與類型體系相關(guān)的元函數(shù)\h4.2設(shè)計理念\h4.2.1支持不同的計算設(shè)備與計算單元\h4.2.2存儲空間的分配與維護(hù)\h4.2.3淺拷貝與寫操作檢測\h4.2.4底層接口擴(kuò)展\h4.2.5類型轉(zhuǎn)換與求值\h4.2.6數(shù)據(jù)接口規(guī)范\h4.3標(biāo)量\h4.3.1類模板的聲明\h4.3.2基于CPU的特化版本\h4.3.3標(biāo)量的主體類型\h4.4矩陣\h4.4.1Matrix類模板\h4.4.2特殊矩陣:平凡矩陣、全零矩陣與獨(dú)熱向量\h4.4.3引入新的矩陣類\h4.5列表\h4.5.1Batch模板\h4.5.2Array模板\h4.5.3重復(fù)與Duplicate模板\h4.6小結(jié)\h4.7練習(xí)\h第5章運(yùn)算與表達(dá)式模板\h5.1表達(dá)式模板簡介\h5.2MetaNN運(yùn)算模板的設(shè)計思想\h5.2.1Add模板的問題\h5.2.2運(yùn)算模板的行為分析\h5.3運(yùn)算分類\h5.4輔助模板\h5.4.1輔助類模板OperElementType_/OperDeviceType_\h5.4.2輔助類模板OperXXX_\h5.4.3輔助類模板OperCateCal\h5.4.4輔助類模板OperOrganizer\h5.4.5輔助類模板OperSeq\h5.5運(yùn)算模板的框架\h5.5.1運(yùn)算模板的類別標(biāo)簽\h5.5.2UnaryOp的定義\h5.6運(yùn)算實(shí)現(xiàn)示例\h5.6.1Sigmoid運(yùn)算\h5.6.2Add運(yùn)算\h5.6.3轉(zhuǎn)置運(yùn)算\h5.6.4折疊運(yùn)算\h5.7MetaNN已支持的運(yùn)算列表\h5.7.1一元運(yùn)算\h5.7.2二元運(yùn)算\h5.7.3三元運(yùn)算\h5.8運(yùn)算的折衷與局限性\h5.8.1運(yùn)算的折衷\h5.8.2運(yùn)算的局限性\h5.9小結(jié)\h5.10練習(xí)\h第6章基本層\h6.1層的設(shè)計理念\h6.1.1層的介紹\h6.1.2層對象的構(gòu)造\h6.1.3參數(shù)矩陣的初始化與加載\h6.1.4正向傳播\h6.1.5存儲中間結(jié)果\h6.1.6反向傳播\h6.1.7參數(shù)矩陣的更新\h6.1.8參數(shù)矩陣的獲取\h6.1.9層的中性檢測\h6.2層的輔助邏輯\h6.2.1初始化模塊\h6.2.2DynamicData類模板\h6.2.3層的常用policy對象\h6.2.4InjectPolicy元函數(shù)\h6.2.5通用I/O結(jié)構(gòu)\h6.2.6通用操作函數(shù)\h6.3層的具體實(shí)現(xiàn)\h6.3.1AddLayer\h6.3.2ElementMulLayer\h6.3.3BiasLayer\h6.4MetaNN已實(shí)現(xiàn)的基本層\h6.5小結(jié)\h6.6練習(xí)\h第7章復(fù)合層與循環(huán)層\h7.1復(fù)合層的接口與設(shè)計理念\h7.1.1基本結(jié)構(gòu)\h7.1.2結(jié)構(gòu)描述語法\h7.1.3policy的繼承關(guān)系\h7.1.4policy的修正\h7.1.5復(fù)合層的構(gòu)造函數(shù)\h7.1.6一個完整的復(fù)合層構(gòu)造示例\h7.2policy繼承與修正邏輯的實(shí)現(xiàn)\h7.2.1policy繼承邏輯的實(shí)現(xiàn)\h7.2.2policy修正邏輯的實(shí)現(xiàn)\h7.3ComposeTopology的實(shí)現(xiàn)\h7.3.1功能介紹\h7.3.2拓?fù)渑判蛩惴ń榻B\h7.3.3ComposeTopology包含的主要步驟\h7.3.4結(jié)構(gòu)描述子句與其劃分\h7.3.5結(jié)構(gòu)合法性檢查\h7.3.6拓?fù)渑判虻膶?shí)現(xiàn)\h7.3.7子層實(shí)例化元函數(shù)\h7.4ComposeKernel的實(shí)現(xiàn)\h7.4.1類模板的聲明\h7.4.2子層對象管理\h7.4.3參數(shù)獲取、梯度收集與中性檢測\h7.4.4參數(shù)初始化與加載\h7.4.5正向傳播\h7.4.6反向傳播\h7.5復(fù)合層實(shí)現(xiàn)示例\h7.6循環(huán)層\h7.6.1GruStep\h7.6.2構(gòu)建RecurrentLayer類模板\h7.6.3RecurrentLayer的使用\h7.7小結(jié)\h7.8練習(xí)\h第8章求值與優(yōu)化\h8.1MetaNN的求值模型\h8.1.1運(yùn)算的層次結(jié)構(gòu)\h8.1.2求值子系統(tǒng)的模塊劃分\h8.2基本求值邏輯\h8.2.1主體類型的求值接口\h8.2.2非主體基本數(shù)據(jù)類型的求值\h8.2.3運(yùn)算模板的求值\h8.2.4DyanmicData與求值\h8.3求值過程的優(yōu)化\h8.3.1避免重復(fù)計算\h8.3.2同類計算合并\h8.3.3多運(yùn)算協(xié)同優(yōu)化\h8.4小結(jié)\h8.5練習(xí)第一部分元編程基礎(chǔ)技術(shù)

第1章基本技巧本章將討論元編程與編譯期計算所涉及的基本方法。我們首先介紹元函數(shù),通過簡單的示例介紹編譯期與運(yùn)行期所使用“函數(shù)”的異同。其次,在此基礎(chǔ)上進(jìn)一步討論基本的順序、分支、循環(huán)代碼的書寫方式。最后介紹一種經(jīng)典的技巧——奇特的遞歸模板式。上述內(nèi)容可以視為基本的元編程技術(shù)。而本文后續(xù)章節(jié)也可以視為這些技術(shù)的應(yīng)用。掌握好本章所討論的技術(shù),是熟練使用C++模板元編程與編譯期計算的前提。1.1元函數(shù)與type_traits1.1.1元函數(shù)介紹C++元編程是一種典型的函數(shù)式編程,函數(shù)在整個編程體系中處于核心的地位。這里的函數(shù)與一般C++程序中定義與使用的函數(shù)有所區(qū)別,更接近數(shù)學(xué)意義上的函數(shù)——是無副作用的映射或變換:在輸入相同的前提下,多次調(diào)用同一個函數(shù),得到的結(jié)果也是相同的。如果函數(shù)存在副作用,那么通常是由于存在某些維護(hù)了系統(tǒng)狀態(tài)的變量而導(dǎo)致的。每次函數(shù)調(diào)用時,即使輸入相同,但系統(tǒng)狀態(tài)的差異會導(dǎo)致函數(shù)輸出結(jié)果不同:這樣的函數(shù)被稱為具有副作用的函數(shù)。元函數(shù)會在編譯期被調(diào)用與執(zhí)行。在編譯階段,編譯器只能構(gòu)造常量作為其中間結(jié)果,無法構(gòu)造并維護(hù)可以記錄系統(tǒng)狀態(tài)并隨之改變的量,因此編譯期可以使用的函數(shù)(即元函數(shù))只能是無副作用的函數(shù)。以下代碼定義了一個函數(shù),滿足無副作用的限制,可以作為元函數(shù)使用。1constexprintfun(inta){returna+1;}

其中的constexpr為C++11中的關(guān)鍵字,表明這個函數(shù)可以在編譯期被調(diào)用,是一個元函數(shù)。如果去掉了這個關(guān)鍵字,那么函數(shù)fun將只能用于運(yùn)行期,雖然它具有無副作用的性質(zhì),但也無法在編譯期被調(diào)用。作為一個反例,考慮如下的程序:1staticintcall_count=3;

2constexprintfun2(inta)

3{

4returna+(call_count++);

5}

這個程序片斷無法通過編譯——它是錯誤的。原因是函數(shù)內(nèi)部的邏輯喪失了“無副作用”的性質(zhì)——相同輸入會產(chǎn)生不同的輸出;而關(guān)鍵字constexpr則試圖保持函數(shù)的“無副作用”特性,這就導(dǎo)致了沖突。將其進(jìn)行編譯會產(chǎn)生相應(yīng)的編譯錯誤。如果將函數(shù)中聲明的constexpr關(guān)鍵字去掉,那么程序是可以通過編譯的,但fun2無法在編譯期被調(diào)用,因為它不再是一個元函數(shù)了。希望上面的例子能讓讀者對元函數(shù)有一個基本的印象。在C++中,我們使用關(guān)鍵字constexpr來表示數(shù)值元函數(shù),這是C++中涉及的一種元函數(shù),但遠(yuǎn)非全部。事實(shí)上,C++中用得更多的是類型元函數(shù)——即以類型作為輸入和(或)輸出的元函數(shù)。1.1.2類型元函數(shù)從數(shù)學(xué)角度來看,函數(shù)通常可以被寫為如下的形式:其中的3個符號分別表示了輸入(x)、輸出(y)與映射(f)\h\h[1]。通常來說,函數(shù)的輸入與輸出均是數(shù)值。但我們大可不必局限于此:比如在概率論中就存在從事件到概率值的函數(shù)映射,相應(yīng)的輸入是某個事件描述,并不一定要表示為數(shù)值。回到元編程的討論中,元編程的核心是元函數(shù),元函數(shù)輸入、輸出的形式也可以有很多種,數(shù)值是其中的一種,由此衍生出來的就是上一節(jié)所提到的數(shù)值元函數(shù);也可以將C++中的數(shù)據(jù)類型作為函數(shù)的輸入與輸出。考慮如下情形:我們希望將某個整數(shù)類型映射為相應(yīng)的無符號類型。比如,輸入類型int時,映射結(jié)果為unsignedint;而輸入為unsignedlong時,我們希望映射的結(jié)果與輸入相同。這種映射也可以被視作函數(shù),只不過函數(shù)的輸入是int、unsignedlong等類型,輸出是另外的一些類型而已??梢允褂萌缦麓a來實(shí)現(xiàn)上述元函數(shù):1template<typenameT>

2structFun_{usingtype=T;};

3

4template<>

5structFun_<int>{usingtype=unsignedint;};

6

7template<>

8structFun_<long>{usingtype=unsignedlong;};

9

10Fun_<int>::typeh=3;

讀者可能會問:函數(shù)定義在哪兒?最初接觸元函數(shù)的讀者往往會有這樣的疑問。事實(shí)上,上述片斷的1~8行已經(jīng)定義了一個函數(shù)Fun_第10行則使用了這個Fun_<int>::type函數(shù)返回unsignedint,所以第10行相當(dāng)于定義了一個無符號整型的變量h并賦予值3。Fun_與C++一般意義上的函數(shù)看起來完全不同,但根據(jù)前文對函數(shù)的定義,不難發(fā)現(xiàn),F(xiàn)un_具備了一個元函數(shù)所需要的全部性質(zhì):輸入為某個類型信息T,以模板參數(shù)的形式傳遞到Fun_模板中;輸出為Fun_模板的內(nèi)部類型type,即Fun_<T>::type;映射體現(xiàn)為模板通過特化實(shí)現(xiàn)的轉(zhuǎn)換邏輯:若輸入類型為int,則輸出類型為unsignedint,等等。在C++11發(fā)布之前,已經(jīng)有一些討論C++元函數(shù)的著作了。在《C++模板元編程》一書\h\h[2]中,將上述程序段中的1~6行所聲明的Fun_視為元函數(shù):認(rèn)為函數(shù)輸入是X時,輸出為Fun_<X>::type。同時,該書規(guī)定了所討論的元函數(shù)的輸入與輸出均是類型。將一個包含了type聲明的類模板稱為元函數(shù),這一點(diǎn)并無不妥之處:它完全滿足元函數(shù)無副作用的要求。但作者認(rèn)為,這種定義還是過于狹隘了。當(dāng)然像這樣引入限制,相當(dāng)于在某種程度上統(tǒng)一了接口,這將帶來一些程序設(shè)計上的便利性。但作者認(rèn)為這種便利性是以犧牲代碼編寫的靈活性為代價的,成本過高。因此,本文對元函數(shù)的定義并不局限于上述形式。具體來說:并不限制映射的表示形式——像前文所定義的以constexpr開頭的函數(shù),以及本節(jié)討論的提供內(nèi)嵌type類型的模板,乃至后文中所討論的其他形式的“函數(shù)”,只要其無副作用,同時可以在編譯期被調(diào)用,都被本文視為元函數(shù);并不限制輸入與輸出的形式,輸入與輸出可以是類型,數(shù)值甚至是模板。在放松了對元函數(shù)定義的限制的前提下,我們可以在Fun_的基礎(chǔ)上再引入一個定義,從而構(gòu)造出另一個元函數(shù)Fun\h\h[3]:1template<typenameT>

2usingFun=typenameFun_<T>::type;

3

4Fun<int>h=3;

Fun是一個元函數(shù)嗎?如果按照《C++模板元編程》中的定義,它至少不是一個標(biāo)準(zhǔn)的元函數(shù),因為它沒有內(nèi)嵌類型type。但根據(jù)本章開頭的討論,它是一個元函數(shù),因為它具有輸入(T),輸出(Fun<T>),同時明確定義了映射規(guī)則。那么在本文中,就將它視為一個元函數(shù)。事實(shí)上,上文所展示的同時也是C++標(biāo)準(zhǔn)庫中定義元函數(shù)的一種常用的方式。比如,C++11中定義了元函數(shù)std::enable_if,而在C++14中引入了定義std::enable_if_t\h\h[4],前者就像Fun_那樣,是內(nèi)嵌了type類型的元函數(shù),后者則就像Fun那樣,是基于前者給出的一個定義,用于簡化使用。1.1.3各式各樣的元函數(shù)在前文中,我們展示了幾種元函數(shù)的書寫方法,與一般的函數(shù)不同,元函數(shù)本身并非是C++語言設(shè)計之初有意引入的,因此語言本身也沒有對這種構(gòu)造的具體形式給出相應(yīng)的規(guī)定??偟膩碚f,只要確保所構(gòu)造出的映射是“無副作用”的,可以在編譯期被調(diào)用,用于對編譯期乃至運(yùn)行期的程序行為產(chǎn)生影響,那么相應(yīng)的映射都可以被稱為元函數(shù),映射具體的表現(xiàn)形式則可以千變?nèi)f化,并無一定之規(guī)。事實(shí)上,一個模板就是一個元函數(shù)。下面的代碼片斷定義了一個元函數(shù),接收參數(shù)T作為輸入,輸出為Fun<T>:1template<typenameT>

2structFun{};

函數(shù)的輸入可以為空,相應(yīng)地,我們也可以建立無參元函數(shù):1structFun

2{

3usingtype=int;

4};

5

6constexprintfun()

7{

8return10;

9}

這里定義了兩個無參元函數(shù)。前者返回類型int,后者返回數(shù)值10?;贑++14中對constexpr的擴(kuò)展,我們可以按照如下的形式來重新定義1.1.1節(jié)中引入的元函數(shù):1template<inta>

2constexprintfun=a+1;

這看上去越來越不像函數(shù)了,連函數(shù)應(yīng)有的大括號都沒有了。但這確實(shí)是一個元函數(shù)。唯一需要說明的是:現(xiàn)在調(diào)用該函數(shù)的方法與調(diào)用1.1.1節(jié)中元函數(shù)的方法不同了。對于1.1.1節(jié)的函數(shù),我們的調(diào)用方法是fun(3),而對于這個函數(shù),相應(yīng)的調(diào)用方式則變成了fun<3>。除此之外,從編譯期計算的角度來看,這兩個函數(shù)并沒有很大的差異。前文所討論的元函數(shù)均只有一個返回值。元函數(shù)的一個好處是可以具有多個返回值??紤]下面的程序片斷:1template<>

2structFun_<int>

3{

4usingreference_type=int&;

5usingconst_reference_type=constint&;

6usingvalue_type=int;

7};

這是個元函數(shù)嗎?希望你回答“是”。從函數(shù)的角度上來看,它有輸入(int),包含多個輸出:Fun_<int>::reference_type、Fun_<int>::const_reference_type與Fun_<int>::value_type。一些學(xué)者反對上述形式的元函數(shù),認(rèn)為這種形式增加了邏輯間的耦合,從而會對程序設(shè)計產(chǎn)生不良的影響(見《C++模板元編程》)。從某種意義上來說,這種觀點(diǎn)是正確的。但作者并不認(rèn)為完全不能使用這種類型的函數(shù),我們大可不必因噎廢食,只需要在合適的地方選擇合適的函數(shù)形式即可。1.1.4type_traits提到元函數(shù),就不能不提及一個元函數(shù)庫:type_traits。type_traits是由boost引入的,C++11將被納入其中,通過頭文件type_traits來引入相應(yīng)的功能。這個庫實(shí)現(xiàn)了類型變換、類型比較與判斷等功能??紤]如下代碼:1std::remove_reference<int&>::typeh1=3;

2std::remove_reference_t<int&>h2=3;

第1行調(diào)用std::remove_reference這個元函數(shù),將int&變換為int并以之聲明了一個變量;第2行則使用std::remove_reference_t實(shí)現(xiàn)了相同的功能。std::remove_reference與std::remove_reference_t都是定義于type_traits中的元函數(shù),其關(guān)系類似于1.1.2節(jié)中討論的Fun_與Fun。通常來說,編寫泛型代碼往往需要使用這個庫以進(jìn)行類型變換。我們的深度學(xué)習(xí)框架也不例外:本文會使用其中的一些元函數(shù),并在首次使用某個函數(shù)時說明其功能。讀者可以參考《C++標(biāo)準(zhǔn)模板庫》\h\h[5]等書籍來系統(tǒng)性地了解該函數(shù)庫。1.1.5元函數(shù)與宏按前文對函數(shù)的定義,理論上宏也可以被視為一類元函數(shù)。但一般來說,人們在討論C++元函數(shù)時,會把討論范圍的重點(diǎn)限制在constexpr函數(shù)以及使用模板構(gòu)造的函數(shù)上,并不包括宏\h\h[6]。這是因為宏是由預(yù)處理器而非編譯器所解析的,這就導(dǎo)致了很多編譯期間可以利用到的特性,宏無法利用。典型事例是,我們可以使用名字空間將constexpr函數(shù)與函數(shù)模板包裹起來,從而確保它們不會與其他代碼產(chǎn)生名字沖突。但如果使用宏作為元函數(shù)的載體,那么我們將喪失這種優(yōu)勢。也正是這個原因,作者認(rèn)為在代碼中盡量避免使用宏。但在特定情況下,宏還是有其自身優(yōu)勢的。事實(shí)上,在構(gòu)造深度學(xué)習(xí)框架時,本文就會使用宏作為模板元函數(shù)的一個補(bǔ)充。但使用宏還是要非常小心的。最基本的,作者認(rèn)為應(yīng)盡量避免讓深度學(xué)習(xí)框架的最終用戶接觸到框架內(nèi)部所定義的宏,同時確保在宏不再被使用時解除其定義。1.1.6本文中元函數(shù)的命名方式元函數(shù)的形式多種多樣,使用起來也非常靈活。在本文(以及所構(gòu)造的深度學(xué)習(xí)框架)中,我們會用到各種類型的元函數(shù)。這里限定了函數(shù)的命名方式,以使得程序的風(fēng)格達(dá)到某種程度上的統(tǒng)一。在本文中,根據(jù)元函數(shù)返回值形式的不同,元函數(shù)的命名方式也會有所區(qū)別:如果元函數(shù)的返回值要用某種依賴型的名稱表示,那么函數(shù)將被命名為xxx_的形式(以下劃線為其后綴);反之,如果元函數(shù)的返回值可以直接用某種非依賴型的名稱表示,那么元函數(shù)的名稱中將不包含下劃線形式的后綴。以下是一個典型的例子:1template<inta,intb>

2structAdd_{

3constexprstaticintvalue=a+b;

4};

5

6template<inta,intb>

7constexprintAdd=a+b;

8

9constexprintx1=Add_<2,3>::value;

10constexprintx2=Add<2,3>;

其中的1~4行定義了元函數(shù)Add_;6~7行定義了元函數(shù)Add。它們具有相同的功能,只是調(diào)用方式不同:第9與10行分別調(diào)用了兩個元函數(shù),獲取到返回結(jié)果后賦予x1與x2。第9行所獲取的是一個依賴型的結(jié)果(value依賴于Add_存在),相應(yīng)地,被依賴的名稱使用下劃線作為后綴:Add_;而第10行在獲取結(jié)果時沒有采用依賴型的寫法,因此函數(shù)名中沒有下劃線后綴。這種書寫形式并非強(qiáng)制性的,本文選擇這種形式,僅僅是為了風(fēng)格上的統(tǒng)一。1.2模板型模板參數(shù)與容器模板相信在閱讀了上節(jié)之后,讀者已經(jīng)建立起了以下的認(rèn)識:元函數(shù)可以操作類型與數(shù)值;對于元函數(shù)來說,類型與數(shù)值并沒有本質(zhì)上的區(qū)別,它們都可視為一種“數(shù)據(jù)”,可以作為元函數(shù)的輸入與輸出。事實(shí)上,C++元函數(shù)可以操作的數(shù)據(jù)包含3類:數(shù)值、類型與模板,它們統(tǒng)一被稱為“元數(shù)據(jù)”,以示與運(yùn)行期所操作的“數(shù)據(jù)”有所區(qū)別。在上一節(jié)中,我們看到了其中的前兩類,本節(jié)首先簡單討論一下模板類型的元數(shù)據(jù)。1.2.1模板作為元函數(shù)的輸入模板可以作為元函數(shù)的輸入?yún)?shù),考慮下面的代碼:1template<template<typename>classT1,typenameT2>

2structFun_{

3usingtype=typenameT1<T2>::type;

4};

5

6template<template<typename>classT1,typenameT2>

7usingFun=typenameFun_<T1,T2>::type;

8

9Fun<std::remove_reference,int&>h=3;

1~7行定義了元函數(shù)Fun,它接收兩個輸入?yún)?shù):一個模板與一個類型。將類型應(yīng)用于模板之上,獲取到的結(jié)果類型作為返回值。在第9行,使用這個元函數(shù)并以std::remove_reference與int&作為參數(shù)傳入。根據(jù)調(diào)用規(guī)則,這個函數(shù)將返回int,即我們在第9行聲明了一個int類型的變量h并賦予值3。從函數(shù)式程序設(shè)計的角度上來說,上述代碼所定義的Fun是一個典型的高階函數(shù),即以另一個函數(shù)為輸入?yún)?shù)的函數(shù)??梢詫⑵淇偨Y(jié)為如下的數(shù)學(xué)表達(dá)式(為了更明確地說明函數(shù)與數(shù)值的關(guān)系,下式中的函數(shù)以大寫字母開頭,而純粹的數(shù)值則是以小寫字母開頭):Fun(T1,t2)=T1(t2)1.2.2模板作為元函數(shù)的輸出與數(shù)值、類型相似,模板除了可以作為元函數(shù)的輸入,還可以作為元函數(shù)的輸出,但編寫起來會相對復(fù)雜一些??紤]下面的代碼:1template<boolAddOrRemoveRef>structFun_;

2

3template<>

4structFun_<true>{

5template<typenameT>

6usingtype=std::add_lvalue_reference<T>;

7};

8

9template<>

10structFun_<false>{

11template<typenameT>

12usingtype=std::remove_reference<T>;

13};

14

15template<typenameT>

16template<boolAddOrRemove>

17usingFun=typenameFun_<AddOrRemove>::templatetype<T>;

18

19template<typenameT>

20usingRes_=Fun<false>;

21

22Res_<int&>::typeh=3;

代碼的1~13行定義了元函數(shù)Fun_:輸入為true時,其輸出Fun_<true>::type為函數(shù)模板add_lvalue_reference,這個函數(shù)模板可以為類型增加左值引用;輸入為false時,其輸出Fun_<false>::type為函數(shù)模板remove_reference,這個函數(shù)模板可以去除類型中的引用。代碼的15~17行定義了元函數(shù)Fun,與之前的示例類似,F(xiàn)un<bool>是Fun_<bool>::type的簡寫\h\h[7]。注意這里的using用法:為了實(shí)現(xiàn)Fun,我們必須引入兩層template聲明:內(nèi)層(第16行)的template定義了元函數(shù)Fun的模板參數(shù);而外層(第15行)的template則表示了Fun的返回值是一個接收一個模板參數(shù)的模板——這兩層的順序不能搞錯。代碼段的19~20行是應(yīng)用元函數(shù)Fun計算的結(jié)果:輸入為false,輸出結(jié)果保存在Res_中。注意此時的Res_還是一個函數(shù)模板,它實(shí)際上對應(yīng)了std::remove_reference——這個元函數(shù)用于去除類型中的引用。而第22行則是進(jìn)一步使用這個函數(shù)模板(元函數(shù)的調(diào)用)來聲明int型的對象h。如果讀者對這種寫法感到困惑,難以掌握,沒有太大的關(guān)系。因為將模板作為元函數(shù)輸出的實(shí)際應(yīng)用相對較少。但如果讀者在后續(xù)的學(xué)習(xí)與工作中遇到了類似的問題,可以將這一小節(jié)的內(nèi)容作為參考。與上一小節(jié)類似,這里也將整個的處理過程表示為數(shù)學(xué)的形式,如下:Fun(addOrRemove)=T其中的addOrRemove是一個bool值,而T則是Fun的輸出,是一個元函數(shù)。1.2.3容器模板學(xué)習(xí)任何一門程序設(shè)計語言之初,我們通常會首先了解該語言所支持的基本數(shù)據(jù)類型,比如C++中使用int表示帶符號的整數(shù)。在此基礎(chǔ)上,我們會對基本數(shù)據(jù)類型進(jìn)行一次很自然地擴(kuò)展:討論如何使用數(shù)組。與之類似,如果將數(shù)值、類型、模板看成元函數(shù)的操作數(shù),那么前文所討論的就是以單個元素為輸入的元函數(shù)。在本節(jié)中,我們將討論元數(shù)據(jù)的“數(shù)組”表示:數(shù)組中的“元素”可以是數(shù)值、類型或模板??梢杂泻芏喾N方法來表示數(shù)組甚至更復(fù)雜的結(jié)構(gòu)?!禖++模板元編程》一書討論了C++模板元編程庫MPL(BoostC++templateMeta-Programminglibrary)。它實(shí)現(xiàn)了類似STL的功能,使用它可以很好地在編譯期表示數(shù)組、集合、映射等復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。但本文并不打算使用MPL,主要原因是MPL封裝了一些底層的細(xì)節(jié),這些細(xì)節(jié)對于元編程的學(xué)習(xí)來說,又是非常重要的。如果簡單地使用MPL,將在一定程度上喪失學(xué)習(xí)元編程技術(shù)的機(jī)會。而另一方面,掌握了基本的元編程方法之后再來看MPL,就會對其有更深入的理解,同時使用起來也會更得心應(yīng)手。這就好像學(xué)習(xí)C++語言時,我們通常會首先討論inta[10]這樣的數(shù)組,并以此引申出指針等重要的概念,在此基礎(chǔ)上再討論vector<int>時,就會有更深入的理解。本文會討論元編程的核心技術(shù),而非一些元編程庫的使用方式。我們只會使用一些自定義的簡單結(jié)構(gòu)來表示數(shù)組,就像int*這樣,簡單易用。從本質(zhì)上來說,我們需要的并非一種數(shù)組的表示方式,而是一個容器:用來保存數(shù)組中的每個元素。元素可以是數(shù)值、類型或模板??梢詫⑦@3種數(shù)據(jù)視為不同類別的操作數(shù),就像C++中的int與float屬于不同的類型。在元函數(shù)中,我們也可以簡單地認(rèn)為“數(shù)值”與“類型”屬于不同的類別。典型的C++數(shù)組(無論是int*還是vector<int>)都僅能保存一種類型的數(shù)據(jù)。這樣設(shè)計的原因首先是實(shí)現(xiàn)比較簡單,其次是它能滿足大部分的需求。與之類似,我們的容器也僅能保存一種類別的操作數(shù),比如一個僅能保存數(shù)值的容器,或者僅能保存類型的容器,或者僅能保存模板的容器。這種容器已經(jīng)能滿足絕大多數(shù)的使用需求了。C++11中引入了變長參數(shù)模板(variadictemplate),使用它可以很容易地實(shí)現(xiàn)我們需要的容器\h\h[8]:1template<int...Vals>structIntContainer;

2template<bool...Vals>structBoolContainer;

3

4template<typename...Types>structTypeContainer;

5

6template<template<typename>class...T>structTemplateCont;

7template<template<typename...>class...T>structTemplateCont2;

上面的代碼段聲明了5個容器(相當(dāng)于定義了5個數(shù)組)。其中前兩個容器分別可以存放int與bool類型的變量;第3個容器可以存放類型;第4個容器可以存放模板作為其元素,每個模板元素可以接收一個類型作為參數(shù);第5個容器同樣以模板作為其元素,但每個模板可以放置多個類型信息\h\h[9]。細(xì)心的讀者可能發(fā)現(xiàn),上面的5條語句實(shí)際上是聲明而非定義(每個聲明的后面都沒有跟著大括號,因此僅僅是聲明)。這也是C++元編程的一個特點(diǎn):事實(shí)上,我們可以將每條語句最后加上大括號,形成定義。但思考一下,我們需要定義嗎?不需要。聲明中已經(jīng)包含了編譯器需要使用的全部信息,既然如此,為什么還要引入定義呢?事實(shí)上,這幾乎可以稱為元編程中的一個慣用法了——僅在必要時才引入定義,其他的時候直接使用聲明即可。在后文中,我們會看到很多類似的聲明,并通過具體的示例來了解這些聲明的使用方式。事實(shí)上,到目前為止,我們已經(jīng)基本完成了數(shù)據(jù)結(jié)構(gòu)的討論——深度學(xué)習(xí)框架只需要使用上述數(shù)據(jù)結(jié)構(gòu)就可以完成構(gòu)造了。如果你對這些結(jié)構(gòu)還不熟悉,沒關(guān)系,在后面構(gòu)造深度學(xué)習(xí)框架的過程中,我們會不斷地使用上述數(shù)據(jù)結(jié)構(gòu),你也就會不斷地熟悉它們。數(shù)據(jù)結(jié)構(gòu)僅僅是故事的一半,一個完整的程序除了數(shù)據(jù)結(jié)構(gòu)還要包含算法。而算法則是由最基本的順序、分支與循環(huán)操作構(gòu)成的。在下一節(jié),我們將討論涉及元函數(shù)時,該如何編寫相應(yīng)的順序、分支或循環(huán)邏輯。1.3順序、分支與循環(huán)代碼的編寫相信本文的讀者可以熟練地寫出在運(yùn)行期順序、分支與循環(huán)執(zhí)行的代碼。但本文還是需要單獨(dú)開辟出一節(jié)來討論這個問題,是因為一旦涉及元函數(shù),相應(yīng)的代碼編寫方法也會隨之改變。1.3.1順序執(zhí)行的代碼順序執(zhí)行的代碼書寫起來是比較直觀的,考慮如下代碼:1template<typenameT>

2structRemoveReferenceConst_{

3private:

4usinginter_type=typenamestd::remove_reference<T>::type;

5public:

6usingtype=typenamestd::remove_const<inter_type>::type;

7};

8

9template<typenameT>

10usingRemoveReferenceConst

11=typenameRemoveReferenceConst_<T>::type;

12

13RemoveReferenceConst<constint&>h=3;

這一段代碼的重點(diǎn)是2~7行,它封裝了元函數(shù)RemoveReferenceConst_,這個函數(shù)內(nèi)部則包含了兩條語句,順序執(zhí)行:(1)第4行根據(jù)T計算出inter_type;(2)第6行根據(jù)inter_type計算出type。同時,代碼中的inter_type被聲明為private類型,以確保函數(shù)的使用者不會誤用inter_type這個中間結(jié)果作為函數(shù)的返回值。這種順序執(zhí)行的代碼很好理解,唯一需要提醒的是,現(xiàn)在結(jié)構(gòu)體中的所有聲明都要看成執(zhí)行的語句,不能隨意調(diào)換其順序??紤]下面的代碼:1structRunTimeExample{

2staticvoidfun1(){fun2();}

3staticvoidfun2(){cerr<<"hello"<<endl;}

4};

這段代碼是正確的,可以將fun1與fun2的定義順序發(fā)生調(diào)換,不會改變它們的行為。但如果我們將元編程示例中的代碼調(diào)整順序:1template<typenameT>

2structRemoveReferenceConst_{

3usingtype=typenamestd::remove_const<inter_type>::type;

4usinginter_type=typenamestd::remove_reference<T>::type;

5};

程序?qū)o法編譯,這并不難理解:在編譯期,編譯器會掃描兩遍結(jié)構(gòu)體中的代碼,第一遍處理聲明,第二遍才會深入到函數(shù)的定義之中。正因為如此,RunTimeExample是正確的,第一遍掃描時,編譯器只是了解到RunTimeExample包含了兩個成員函數(shù)fun1與fun2;在后續(xù)的掃描中,編譯器才會關(guān)注fun1中調(diào)用了fun2。雖然fun2的調(diào)用語句出現(xiàn)在其聲明之前,但正是因為這樣的兩遍掃描,編譯器并不會報告找不到fun2這樣的錯誤。但修改后的RemoveReferenceConst_中,編譯器在首次從前到后掃描程序時,就會發(fā)現(xiàn)type依賴于一個沒有定義的inter_type,它不繼續(xù)掃描后續(xù)的代碼,而是會直接給出錯誤信息。在很多情況下,我們會將元函數(shù)的語句置于結(jié)構(gòu)體或類中,此時就要確保其中的語句順序正確。1.3.2分支執(zhí)行的代碼我們也可以在編譯期引入分支的邏輯。與編譯期順序執(zhí)行的代碼不同的是,編譯期的分支邏輯既可以表現(xiàn)為純粹的元函數(shù),也可以與運(yùn)行期的執(zhí)行邏輯相結(jié)合。對于后者,編譯期的分支往往用于運(yùn)行期邏輯的選擇。我們將在這一小節(jié)看到這兩種情形各自的例子。事實(shí)上,在前面的討論中,我們已經(jīng)實(shí)現(xiàn)過分支執(zhí)行的代碼了。比如在1.2.2節(jié)中,實(shí)現(xiàn)了一個Fun_元函數(shù),并使用一個bool參數(shù)來決定函數(shù)的行為(返回值):這就是一種典型的分支行為。事實(shí)上,像該例那樣,使用模板的特化或部分特化來實(shí)現(xiàn)分支,是一種非常常見的分支實(shí)現(xiàn)方式。當(dāng)然,除此之外,還存在一些其他的分支實(shí)現(xiàn)方式,每種方式都有自己的優(yōu)缺點(diǎn)——本小節(jié)會討論其中的幾種。使用std::conditional與std::conditional_t實(shí)現(xiàn)分支conditional與conditional_t是type_traits中提供的兩個元函數(shù),其定義如下\h\h[10]:1namespacestd

2{

3template<boolB,typenameT,typenameF>

4structconditional{

5usingtype=T;

6};

7

8template<typenameT,typenameF>

9structconditional<false,T,F>{

10usingtype=F;

11};

12

13template<boolB,typenameT,typenameF>

14usingconditional_t=typenameconditional<B,T,F>::type;

15}

其邏輯行為是:如果B為真,則函數(shù)返回T,否則返回F。其典型的使用方式為:1std::conditional<true,int,float>::typex=3;

2std::conditional_t<false,int,float>y=1.0f;

分別定義了int型的變量x與float型的變量y。conditional與conditional_t的優(yōu)勢在于使用比較簡單,但缺點(diǎn)是表達(dá)能力不強(qiáng):它只能實(shí)現(xiàn)二元分支(真假分支),其行為更像運(yùn)行期的問號表達(dá)式:x=B?T:F;。對于多元分支(類似于switch的功能)則支持起來就比較困難了。相應(yīng)地,conditional與conditional_t的使用場景是相對較少的。除非是特別簡單的分支情況,否則并不建議使用這兩個元函數(shù)。使用(部分)特化實(shí)現(xiàn)分支在前文的討論中,我們就是使用特化來實(shí)現(xiàn)的分支。(部分)特化天生就是用來引入差異的,因此,使用它來實(shí)現(xiàn)分支也是十分自然的??紤]下面的代碼:1structA;structB;

2

3template<typenameT>

4structFun_{

5constexprstaticsize_tvalue=0;

6};

7

8template<>

9structFun_<A>{

10constexprstaticsize_tvalue=1;

11};

12

13template<>

14structFun_<B>{

15constexprstaticsize_tvalue=2;

16};

17

18constexprsize_th=Fun_<B>::value;

代碼的第18行根據(jù)元函數(shù)Fun_的輸入?yún)?shù)不同,為h賦予了不同的值——這是一種典型的分支行為。Fun_元函數(shù)實(shí)際上引入了3個分支,分別對應(yīng)輸入?yún)?shù)為A、B與默認(rèn)的情況。使用特化引入分支代碼書寫起來比較自然,容易理解,但代碼一般比較長。在C++14中,除了可以使用上述方法進(jìn)行特化,還可以有其他的特化方式,考慮下面的代碼:1structA;structB;

2

3template<typenameT>

4constexprsize_tFun=0;

5

6template<>

7constexprsize_tFun<A>=1;

8

9template<>

10constexprsize_tFun<B>=2;

11

12constexprsize_th=Fun<B>;

這段代碼與上一段實(shí)現(xiàn)了相同的功能(唯一的區(qū)別是元函數(shù)調(diào)用時,前者需要給出依賴型名稱::value,而后者則無須如此),但實(shí)現(xiàn)簡單一些。如果希望分支返回的結(jié)果是單一的數(shù)值,則可以考慮這種方式。使用特化來實(shí)現(xiàn)分支時,有一點(diǎn)需要注意:在非完全特化的類模板中引入完全特化的分支代碼是非法的??紤]如下代碼:1template<typenameTW>

2structWrapper{

3template<typenameT>

4structFun_{

5constexprstaticsize_tvalue=0;

6};

7

8template<>

9structFun_<int>{

10constexprstaticsize_tvalue=1;

11};

12};

這個程序是非法的。原因是Wrapper是一個未完全特化的類模板,但在其內(nèi)部包含了一個模板的完全特化Fun_<int>,這是C++標(biāo)準(zhǔn)所不允許的,會產(chǎn)生編譯錯誤。為了解決這個問題,我們可以使用部分特化來代替完全特化,將上面的代碼修改如下:1template<typenameTW>

2structWrapper{

3template<typenameT,typenameTDummy=void>

4structFun_{

5constexprstaticsize_tvalue=0;

6};

7

8template<typenameTDummy>

9structFun_<int,TDummy>{

10constexprstaticsize_tvalue=1;

11};

12};

這里引入了一個偽參數(shù)TDummy,用于將原有的完全特化修改為部分特化。這個參數(shù)有一個默認(rèn)值void,這樣就可直接以Fun_<int>的形式調(diào)用這個元函數(shù),無需為偽參數(shù)賦值了。使用std::enable_if與std::enable_if_t實(shí)現(xiàn)分支enable_if與enable_if_t的定義如下:1namespacestd

2{

3template<boolB,typenameT=void>

4structenable_if{};

5

6template<classT>

7structenable_if<true,T>{usingtype=T;};

8

9template<boolB,classT=void>

10usingenable_if_t=typenameenable_if<B,T>::type;

11}

對于分支的實(shí)現(xiàn)來說,這里面的T并不特別重要,重要的是當(dāng)B為true時,enable_if元函數(shù)可以返回結(jié)果type??梢曰谶@個構(gòu)造實(shí)現(xiàn)分支,考慮下面的代碼:1template<boolIsFeedbackOut,typenameT,

2std::enable_if_t<IsFeedbackOut>*=nullptr>

3autoFeedbackOut_(T&&){/*...*/}

4

5template<boolIsFeedbackOut,typenameT,

6std::enable_if_t<!IsFeedbackOut>*=nullptr>

7autoFeedbackOut_(T&&){/*...*/}

這里引入了一個分支。當(dāng)IsFeedbackOut為真時,std::enable_if_t<IsFeedbackOut>::type是有意義的,這就使得第一個函數(shù)匹配成功;與之相應(yīng)的,第二個函數(shù)匹配是失敗的。反之,當(dāng)IsFeedbackOut為假時,std::enable_if_t<!IsFeedbackOut>::type是有意義的,這就使得第二個函數(shù)匹配成功,第一個函數(shù)匹配失敗。C++中有一個特性SFINAE(SubstitutionFailureIsNotAnError),中文譯為“匹配失敗并非錯誤”。對于上面的程序來說,一個函數(shù)匹配失敗,另一個函數(shù)匹配成功,則編譯器會選擇匹配成功的函數(shù)而不會報告錯誤。這里的分支實(shí)現(xiàn)也正是利用了這個特性。通常來說,enable_if與enable_if_t會被用于函數(shù)之中,用做重載的有益補(bǔ)充——重載通過不同類型的參數(shù)來區(qū)別重名的函數(shù)。但在一些情況下,我們希望引入重名函數(shù),但無法通過參數(shù)類型加以區(qū)分\h\h[11]。此時通過enable_if與enable_if_t就能在一定程度上解決相應(yīng)的重載問題。需要說明的是,enable_if與enable_if_t的使用形式是多種多樣的,并不局限于前文中作為模板參數(shù)的方式。事實(shí)上,只要C++中支持SFINAE的地方,都可以引入enable_if或enable_if_t。有興趣的讀者可以參考C++Reference中的說明。enable_if或enable_if_t也是有缺點(diǎn)的:它并不像模板特化那樣直觀,以之書寫的代碼閱讀起來也相對困難一些(相信了解模板特化機(jī)制的程序員比了解SFINAE的還是多一些的)。還要說明的一點(diǎn)是,這里給出的基于enable_if的例子就是一個典型的編譯期與運(yùn)行期結(jié)合的使用方式。FeedbackOut_中包含了運(yùn)行期的邏輯,而選擇哪個FeedbackOut_則是通過編譯期的分支來實(shí)現(xiàn)的。通過引入編譯期的分支方法,我們可以創(chuàng)造出更加靈活的函數(shù)。編譯期分支與多種返回類型編譯期分支代碼看上去比運(yùn)行期分支復(fù)雜一些,但與運(yùn)行期相比,它也更加靈活??紤]如下代碼:1autowrap1(boolCheck)

2{

3if(Check)return(int)0;

4elsereturn(double)0;

5}

這是一個運(yùn)行期的代碼。首先要對第1行的代碼簡單說明一下:在C++14中,函數(shù)聲明中可以不用顯式指明其返回類型,編譯器可以根據(jù)函數(shù)體中的return語句來自動推導(dǎo)其返回類型,但要求函數(shù)體中的所有return語句所返回的類型均相同。對于上述代碼來說,其第3行與第4行返回的類型并不相同,這會導(dǎo)致編譯出錯。事實(shí)上,對于運(yùn)行期的函數(shù)來說,其返回類型在編譯期就已經(jīng)確定了,無論采用何種寫法,都無法改變。但在編譯期,我們可以在某種程度上打破這樣的限制:1template<boolCheck,std::enable_if_t<Check>*=nullptr>

2autofun(){

3return(int)0;

4}

5

6template<boolCheck,std::enable_if_t<!Check>*=nullptr>

7autofun(){

8return(double)0;

9}

10

11template<boolCheck>

12autowrap2(){

13returnfun<Check>();

14}

15

16intmain(){

17std::cerr<<wrap2<true>()<<std::endl;

18}

wrap2的返回值是什么呢?事實(shí)上,這要根據(jù)模板參數(shù)Check的值來決定。通過C++中的這個新特性以及編譯期的計算能力,我們實(shí)現(xiàn)了一種編譯期能夠返回不同類型的數(shù)據(jù)結(jié)果的函數(shù)。當(dāng)然,為了執(zhí)行這個函數(shù),我們還是需要在編譯期指定模板參數(shù)值,從而將這個編譯期的返回多種類型的函數(shù)蛻化為運(yùn)行期的返回單一類型的函數(shù)。但無論如何,通過上述技術(shù),編譯期的函數(shù)將具有更強(qiáng)大的功能,這種功能對元編程來說是很有用的。這也是一個編譯期分支與運(yùn)行期函數(shù)相結(jié)合的例子。事實(shí)上,通過元函數(shù)在編譯期選擇正確的運(yùn)行期函數(shù)是一種相對常見的編程方法,因此C++17專門引入了一種新的語法ifconstexpr來簡化代碼的編寫。使用ifconstexpr簡化代碼對于上面的代碼段來說,在C++17中可以簡化為:1template<boolCheck>

2autofun()

3{

4ifconstexpr(Check)

5{

6return(int)0;

7}

8else

9{

10return(double)0;

11}

12}

13

14intmain(){

15std::cerr<<fun<true>()<<std::endl;

16}

其中的ifconstexpr必須接收一個常量表達(dá)式,即編譯期常量。編譯器在解析到相關(guān)的函數(shù)調(diào)用時,會自動選擇ifconstexpr表達(dá)式為真的語句體,而忽略其他的語句體。比如,在編譯器解析到第15行的函數(shù)調(diào)用時,會自動構(gòu)造類似如下的函數(shù):1//template<boolCheck>

2autofun()

3{

4//ifconstexpr(Check)

5//{

6return(int)0;

7//}

8//else

9//{

10//return(double)0;

11//}

12}

使用ifconstexpr寫出的代碼與運(yùn)行期的分支代碼更像。同時,它有一個額外的好處,就是可以減少編譯實(shí)例的產(chǎn)生。使用上一節(jié)中編寫的代碼,編譯器在進(jìn)行一次實(shí)例化時,需要構(gòu)造wrap2與fun兩個實(shí)例;但使用本節(jié)的代碼,編譯器在實(shí)例化時只會產(chǎn)生一個fun函數(shù)的實(shí)例。雖然優(yōu)秀的編譯器可以通過內(nèi)聯(lián)等方式對構(gòu)造的實(shí)例進(jìn)行合并,但我們并不能保證編譯器一定會這樣處理。反過來,使用ifconstexpr則可以確保減少編譯器所構(gòu)造的實(shí)例數(shù),這也就意味著在一定程度上減少編譯所需要的資源以及編譯產(chǎn)出的文件大小。但ifconstexpr也有缺點(diǎn)。首先,如果我們在編程時忘記書寫constexpr,那么某些函數(shù)也能通過編譯,但分支的選擇則從編譯期轉(zhuǎn)換到了運(yùn)行期——此時,我們還是會在運(yùn)行期引入相應(yīng)的分支選擇,無法在編譯期將其優(yōu)化掉。其次,ifconstexpr的使用場景相對較窄:它只能放在一般意義上的函數(shù)內(nèi)部,用于在編譯期選擇所執(zhí)行的代碼。如果我們希望構(gòu)造元函數(shù),通過分支來返回不同的類型作為結(jié)果,那么ifconstexpr就無能為力了。該在什么情況下使用ifconstexpr,還需要針對特定的問題具體分析。1.3.3循環(huán)執(zhí)行的代碼一般來說,我們不會用while、for這樣的語句組織元函數(shù)中的循環(huán)代碼——因為這些代碼操作的是變量。但在編譯期,我們操作的更多的則是常量、類型與模板\h\h[12]。為了能夠有效地操縱元數(shù)據(jù),我們往往會使用遞歸的形式來實(shí)現(xiàn)循環(huán)。還是讓我們參考一個例子:給定一個無符號整數(shù),求該整數(shù)所對應(yīng)的二進(jìn)制表示中1的個數(shù)。在運(yùn)行期,我們可以使用一個簡單的循環(huán)來實(shí)現(xiàn)。在編譯期,我們就需要使用遞歸來實(shí)現(xiàn)了:1template<size_tInput>

2constexprsize_tOnesCount=(Input%2)+OnesCount<(Input/2)>;

3

4template<>constexprsize_tOnesCount<0>=0;

5

6constexprsize_tres=OnesCount<45>;

1~4行定義了元函數(shù)OnesCount,第6行則使用了這個元函數(shù)計算45對應(yīng)的二進(jìn)制包含的1的個數(shù)。你可能需要一段時間才能適應(yīng)這種編程風(fēng)格。整個程序在邏輯上并不復(fù)雜,它使用了C++14中的特性,代碼量也與編寫一個while循環(huán)相差無幾。程序第2行OnesCount<(Input/2)>是其核心,它本質(zhì)上是一個遞歸調(diào)用。讀者可以思考一下,當(dāng)Input為45或者任意其他的數(shù)值時,代碼段第2行的行為。一般來說,在采用遞歸實(shí)現(xiàn)循環(huán)的元程序中,需要引入一個分支來結(jié)束循環(huán)。上述程序的第4行實(shí)現(xiàn)了這一分支:當(dāng)將輸入減小到0時,程序進(jìn)入這一分支,結(jié)束循環(huán)。循環(huán)使用更多的一類情況則是處理數(shù)組元素。我們在前文中討論了數(shù)組的表示方法,在這里,給出一個處理數(shù)組的示例:1template<size_t...Inputs>

2constexprsize_tAccumulate=0;

3

4template<size_tCurInput,size_t...Inputs>

5constexprsize_tAccumulate<CurInput,Inputs...>

6=CurInput+Accumulate<Inputs...>;

7

8constexprsize_tres=Accumulate<1,2,3,4,5>;

1~6行定義了一個元函數(shù):Accumulate,它接收一個size_t類型的數(shù)組,對數(shù)組中的元素求和并將結(jié)果作為該元函數(shù)的輸出。第8行展示了該元函數(shù)的用法:計算res的值15。正如前文所述,在元函數(shù)中引入循環(huán),非常重要的一點(diǎn)是引入一個分支來終止循環(huán)。程序的第2行是用于終止循環(huán)的分支:當(dāng)輸入數(shù)組為空時,會匹配這個函數(shù)的模板參數(shù)<size_t...Inputs>,此時Accumulate返回0。而4~6行則組成了另一個分支:如果數(shù)組中包含一個或多于一個的元素,那么調(diào)用Accumulate將匹配第二個模板特化,取出首個元素,將剩余元素求和后加到首個元素之上。事實(shí)上,僅就本例而言,在C++17中可以有更簡單的代碼編寫方法,即使用其所提供的foldexpression技術(shù):1template<size_t...values>

2constexprsize_tfun()

3{

4return(0+...+values);

5}

6

7constexprsize_tres=fun<1,2,3,4,5>();

foldexpression本質(zhì)上也是一種簡化的循環(huán)寫法,它的使用具有一定的限制。本文不對其進(jìn)行重點(diǎn)討論。編譯期的循環(huán),本質(zhì)上是通過分支對遞歸代碼進(jìn)行控制的。因此,上一節(jié)所討論的很多分支編寫方法也可以衍生并編寫相應(yīng)的循環(huán)代碼。典型的,可以使用ifconstexpr來編寫分支,這項工作就留給讀者進(jìn)行練習(xí)了。1.3.4小心:實(shí)例化爆炸與編譯崩潰回顧一下之前的代碼:1template<size_tInput>

2constexprsize_tOnesCount=(Input%2)+OnesCount<(Input/2)>;

3

4template<>constexprsize_tOnesCount<0>=0;

5

6constexprsize_tx1=OnesCount<7>;

7constexprsize_tx1=OnesCount<15>;

考慮一下,編譯器在編譯這一段時,會產(chǎn)生多少個實(shí)例。在第6行以7為模板參數(shù)傳入時,編譯器將使用7、3、1、0來實(shí)例化OnesCount,構(gòu)造出4個實(shí)例。接下來第7行以15為參數(shù)傳入這個模板,那么編譯器需要用15、7、3、1、0來實(shí)例化代碼。通常,編譯器會將第一次使用7、3、1、0實(shí)例化出的代碼保留起來,這樣一來,如果后面的編譯過程中需要使用同樣的實(shí)例,那么之前保存的實(shí)例就可以復(fù)用了。對于一般的C++程序來說,這樣做能極大地提升編譯速度,但對于元編程來說,這可能會造成災(zāi)難。考慮以下的代碼:1template<size_tA>

2structWrap_{

3template<size_tID,typenameTDummy=void>

4structimp{

5constexprstaticsize_tvalue=ID+imp<ID-1>::value;

6};

7

8template<typenameTDummy>

9structimp<0,TDummy>{

10constexprstaticsize_tvalue=0;

11};

12

13template<size_tID>

14constexprstaticsize_tvalue=imp<A+ID>::value;

15};

16

17intmain(){

18std::cerr<<Wrap_<3>::value<2><<std::endl;

19std::cerr<<Wrap_<10>::value<2><<std::endl;

20}

這段代碼結(jié)合了前文所討論的分支與循環(huán)技術(shù),構(gòu)造出了Wrap_類模板。它是一個元函數(shù),接收參數(shù)A返回另一個元函數(shù)。后者接收參數(shù)ID,并計算。在編譯第18行代碼時,編譯器會因為這條語句產(chǎn)生Wrap_<3>::imp的一系列實(shí)例。不幸的是,在編譯第19行代碼時,編譯器無法復(fù)用這些實(shí)例,因為它所需要的是Wrap_<10>::imp的一系列實(shí)例,這與Wrap_<3>::imp系列并不同名。因此,我們無法使用編譯器已經(jīng)編譯好的實(shí)例來提升編譯速度。實(shí)際情況可能會更糟,編譯器很可能會保留Wrap_<3>::imp的一系列實(shí)例,因為它會假定后續(xù)可能還會出現(xiàn)再次需要該實(shí)例的情形。上例中Wrap_中包含了一個循環(huán),循環(huán)所產(chǎn)生的全部實(shí)例都會在編譯器中保存。如果我們的元函數(shù)中包含了循環(huán)嵌套,那么由此產(chǎn)生的實(shí)例將隨循環(huán)層數(shù)的增加呈指數(shù)的速度增長——這些內(nèi)容都會被保存在編譯器中。不幸的是,編譯器的設(shè)計往往是為了滿足一般性的編譯任務(wù),對于元編程這種目前來說使用情形并不多的技術(shù)來說,優(yōu)化相對較少。因此編譯器的開發(fā)者可能不會考慮編譯過程中保存在內(nèi)存中的實(shí)例數(shù)過多的問題(對于非元編程的情況,這可能并不是一個大問題)。但另一方面,如果編譯過程中保存了大量的實(shí)例,那么可能會導(dǎo)致編譯器的內(nèi)存超限,從而出現(xiàn)編譯失敗甚至崩潰的情況。這并非危言聳聽。事實(shí)上,在作者編寫深度學(xué)習(xí)框架時,就出現(xiàn)過對這個問題沒有引起足夠重視,而導(dǎo)致編譯內(nèi)存占用過多,最終編譯失敗的情況。在小心修改了代碼之后,編譯所需的內(nèi)存比之前減少了50%以上,編譯也不再崩潰了。那么如何解決這個問題呢?其實(shí)很簡單:將循環(huán)拆分出來。對于上述代碼,我們可以修改為如下內(nèi)容:1template<size_tID>

2structimp{

3constexprstaticsize_tvalue=ID+imp<ID-1>::value;

4};

5

6template<>

7structimp<0>{

8constexprstaticsize_tvalue=0;

9};

10

11template<size_tA>

12structWrap_{

13template<size_tID>

14constexprstaticsize_tvalue=imp<A+ID>::value;

15};

在實(shí)例化Wrap_<3>::value<2>時,編譯器會以5、4、3、2、1、0為參數(shù)構(gòu)造imp。在隨后實(shí)例化Wrap_<10>::value<2>時,之前構(gòu)造的東西還可以被使用,新的實(shí)例化次數(shù)也會隨之變少。但這種修改還是有不足之處的:在之前的代碼中,imp被置于Wrap_中,這表明了二者的緊密聯(lián)系;從名稱污染的角度上來說,這樣做不會讓imp污染W(wǎng)rap_外圍的名字空間。但在后一種實(shí)現(xiàn)中,imp將對名字空間造成污染:在相同的名字空間中,我們無法再引入另一個名為imp的構(gòu)造,供其他元函數(shù)調(diào)用。如何解決這種問題呢?這實(shí)際上是一種權(quán)衡。如果元函數(shù)的邏輯比較簡單,同時并不會產(chǎn)生大量實(shí)例,那么保留前一種(對編譯器來說比較糟糕的)形式,可能并不會對編譯器產(chǎn)生太多負(fù)面的影響,同時使得代碼具有更好的內(nèi)聚性。反之,如果元函數(shù)邏輯比較復(fù)雜(典型情況是多重循環(huán)嵌套),又可能會產(chǎn)生很多實(shí)例,那么就選擇后一種方式以節(jié)省編譯資源。即使選擇后一種方式,我們也應(yīng)當(dāng)盡力避免名字污染。為了解決這個問題,在后續(xù)編寫深度學(xué)習(xí)框架時,我們會引入專用的名字空間,來存放像imp這樣的輔助代碼。1.3.5分支選擇與短路邏輯減少編譯期實(shí)例化的另一種重要的技術(shù)就是引入短路邏輯??紤]如下代碼:1template<size_tN>

2constexprboolis_odd=((N%2)==1);

3

4template<size_tN>

5structAllOdd_{

6constexprstaticboolis_cur_odd=is_odd<N>;

7constexprstaticboolis_pre_odd=AllOdd_<N-1>::value;

8constexprstaticboolvalue=is_cur_odd&&is_pre_odd;

9};

10

11template<>

12structAllOdd_<0>{

13constexprstaticboolvalue=is_odd<0>;

14};

這段代碼的邏輯并不復(fù)雜。1~2行引入了一個元函數(shù)is_odd,用來判斷一個數(shù)是否為奇數(shù)。在此基礎(chǔ)上,AllOdd_用于給定數(shù)N,判斷0~N的數(shù)列中是否每個數(shù)均為奇數(shù)。雖然這段代碼的邏輯非常簡單,但足以用于討論本節(jié)中的問題了??紤]一下在上述代碼中,為了進(jìn)行判斷,編譯器進(jìn)行了多少次實(shí)例化。在代碼段的第7行,系統(tǒng)進(jìn)行了遞歸的實(shí)例化。給定N作為AllOdd_的輸入時,系統(tǒng)會實(shí)例化

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論