本文按計劃分為上下兩章,分別是基礎編程和技巧。基礎編程介紹了如何用模板做一些簡單的編程。技巧部分會說一些模板元編程的技法。
程式語言和編譯時一個簡單的程式語言通常由什麼要素組成?這個問題很好回答,那就是數據和流程。其中我們把函數為fisrt class的程式語言的函數也叫做數據。
而總所周知,模板是一個圖靈完備的編譯時語言,那麼編譯時的數據有什麼呢?
我們看一個非常簡單的模板。
template<class T>
struct S1 {
using type = T;
};
S1<int>;
這個模板很簡單, 我們想想他幹了什麼。比如說我們用 S1<int> , 他接受一個int類型作為參數,返回一個int類型。在念一遍, 他接受一個int類型作為參數,返回一個int類型,有點像什麼?對, 函數。
此處我們假設返回值是結構體裡的欄位type方便描述參數是一個類型,返回值是一個類型的函數。我們可以這麼寫這個模板
S1 = (T)=>{return T};
S1(int) 和 S1<int> 基本就一回事了。
這是這個程式語言最基本的東西--函數。那麼很自然的 這裡面的類型就是數據了。
而且總所周知的是,模板除了可以用類型為參數,他還接受包括 常量整數值(包括枚舉)、指向對象/函數/成員的指針、指向對象或函數的lvalue引用,或者std::nullptr_t (nullptr的類型)在內的這些編譯時知道是什麼的類型。
為什麼不接受列數據的指針?因為指針在編譯的時候需要連結才能知道是啥。而函數對象啥的都定義在頭文件了template<int V>
struct S2 {
int value = V + 1;
};
比如這麼一個模板 他就是接受一個整數V返回一個value為V+1的函數。
S2 = (T)=>{return V+1};
而且令人感動的是,模板編程裡的函數是一個一等公民。也就是說模板也可以作為數據。不過這裡不可以作為返回值。
template<typename T1, template<typename> class C>
struct S3
{
using type = C<T1>;
};
S3<int, S1>;
這個就是一個接受一個一個參數的模板為參數,返回一個S3的type
S3 = (T1,C)=>{return C(T1)};
S3(int, S1)
總的來說, 模板就是的函數,接受一個編譯時可計算數據返回編譯時計算結果數據的函數。其實編譯時可計算數據是 類型, 編譯時無需連結常量, 編譯時可計算函數。
額外的,我們可以注意一下constexpr關鍵字。可以用這個聲明一個編譯時可以計算的數據和函數。可以理解成一個不能輸入輸出類型的模板函數。
編譯時的流程控制我們現在有了數據了,我們現在就可以考慮完成流程控制了。來讓我們大聲說出三大流程:順序,分支,循環。
順序流程沒什麼可以說的就是一個模板編譯展開的過程。
重點是分支流程:
分支流程編譯時分支流程, 最大的用處就是根據不同的類型執行不同的操作。是不聽得又有點耳熟?對,重載就是一個最常用的編譯時分支。
我們其實可以把這個操作放到運行時, 類似python很多時候就這麼幹。
def f(a):
if type(a)==str:
xxxx
elif type(a)==int:
yyyyy
這樣做,最大的問題是程序有運行時開銷。在C++中我們可以用重載
void f(int a){
xxxx
}
void f(float b){
xxxx
}
其實這就是編譯時的一種if。類似的我們還有倆個大招 偏特化 和 SFINAE.
先說偏特化, 我們把模板展開的過程叫做特化的話, 偏特化就是特化一部分。我們知道的是,比如我說一個重載做不了的事情。類型T做一件事,類型T*做另一個事情。重載就不能很好的做這件事。模板只要:
template<class T>
struct S1 {
using type = T;
};
template<class T>
struct S1<T*> {
using type = T;
};
這句話的意思就是, 如果類型T是指針,那麼type就是去掉指針類型的T,否則type就是T, 寫成函數就是。
S1=(T)=>{
if(T是指針){
return T去除指針;
}
return T
}
為什麼指針類型能匹配到特化而不是什麼都能匹配的T呢?這就是特化的時候會選擇最合適的那個的原則。很符合直覺。
你看到這個往往會覺得眼熟, 如果你看過 《STL源碼剖析》,你會很熟悉。對就是type trait. 這個有些不太好理解的東西。其實他就是利用偏特化做一些進行了if判斷而已。
SFINAE之前的文章講過
模板編程入門-sfinae基礎
,這裡再簡單說說。
這個機制的核心就是,編譯器在嘗試展開一個模板,如果失敗了,不會報錯,會嘗試展開下一個可能的展開。
我們可以用他做什麼偏特化做不了的事情呢?舉個例子,在編譯時判斷一個類型是否有某個成員。這個用偏特化是做不了的。舉個例子
template <typename T>
void f(typename T::foo) {
xxx
}
template <typename T>
void f( T) {
yyy
}
這個會展開第一個, 如果這時候T沒有foo成員 他會展開失敗, 類型檢查過不了。但是因為SFINAE機制的存在,他不會報錯,而是去展開第二個, 寫成函數就是
f =(T)=>{
if(T有foo){
return xxx;
}
return yyy;
}
淺顯易懂。當然,步入了C++20 我們有了更好的工具來完成這個這個任務,那就是約束概念, 這裡給出參考資料供讀者自行了解
https://zh.cppreference.com/w/cpp/language/constraints
除了這個, 還有個寶貝不能忽略。那就是 if constexpr。
這個可以根據一個編譯時可計算數據來進行編譯時的流程控制。
template <int V>
void f() {
if constexpr(v){
xxxx
}
}
這個就是符合你直覺的if, 只不過發生在了編譯時。
循環流程有了if, 我們就想搞個for。說起了函數,我們很自然的想到了一個東西,對的,遞歸。通過遞歸我們可以輕而易舉實現for.
template <int V>
constexpr int r = V + r<V-1>;
template<>
constexpr int r<0> = 0;
翻譯程函數就是
r =(V)=>{
if (v==0){
return 0
}
return f(V-1)+V;
}
顯而易見。
知道了這個原理, 我們有一個令人開心的語法特性,可變參數模板,參考連結
https://zh.cppreference.com/w/cpp/language/parameter_pack
這個東西可以做很多事情。我們現在先知道這個東西是模板元編程編譯時數據的數組就行。
綜合 實現list我們實現一個簡單的編譯時list, 提供一個按下標訪問。
先想想函數:
list =(T)=>{
if(is_empty(T)){
return {value:T[0], left:null}
}
returm {value:T[0], left:list(T[1:])}
}
value表示當前下標的值,left表示剩下的數組, 函數寫出了很容易, 我們翻譯成模板就行了, 不過因為模板不能空展開, 我們需要一個東西標誌一下null
struct list_null {};
我們隨便定義一個類型就行。然後我們實現這個函數:
template <typename... T>
struct list;
template<typename U,typename... T>
struct list<U,T... > {
using value = U;
using left = list<T...>;
};
template<typename U>
struct list<U> {
using value = U;
using left = list_null;
};
使用的時候,只要
using myl = list<int,int,int,int,float>;
myl::value;
myl::left::value;
即可。
很好 我們可以實現下標訪問了。
先用函數:
list_at =(int a, list T)=>{
if (a==0){
return T::value;
}
return list_at(a-1, T[1:])
}
遞歸, 直到為0的時候返回值就可以了, 思路很簡單, 寫成模板
template<int a, typename T>
struct list_at {
using value = typename list_at<a-1, typename T::left>::value;
};
template<typename T>
struct list_at<0, T> {
using value = typename T::value;
};
使用的時候,只要:
using myl = list<int,double,float,int,float>;
using t = typename list_at<2,myl>::value;
t就是數組存的類型。
綜上所述, 我們進行模板元編程的過程中,只要想出來用函數怎麼寫,然後翻譯成模板就可以了。所以我個菜雞猜測一下, 個人函數式編程的抽象能力決定了模板元編程的水平。
參考文獻因為本人水平低下 所以最好多看看參考資料
[1]. https://zh.cppreference.com/w/cpp/language/templates cpprefence的模板章節
[2]. 現代 C++30講: https://time.geekbang.org/column/intro/256?code=TjUT9y8QEechQ9EIIAVu9Kilsx5u1FrzLLQaF8n3X8A%3D
[3]. 知乎文章 C++ 模板元編程:一種屠龍之技 https://zhuanlan.zhihu.com/p/137853957
[4]. github的 CppTemplateTutorial https://github.com/wuye9036/CppTemplateTutorial
如有勘誤或不懂得地方,點擊原文。