在modern c++中,模板元編程有哪些更方便的寫法?
對於不熟悉新標準的人來說,這是個好問題。C++11以來,Modern C++中對模板元編程是越來越重視的。Modern C++的模板元編程和以往的經典模板元編程(元數據,元函數,元函數類那一套)有很大的區別。這裡談一些自己的心得體會,權當拋磚引玉。
- constexpr
constexpr是給模板元編程帶來最大影響的關鍵字。在沒有constexpr之前,求階乘我們只能用經典模板元編程那一套(熟悉C++的朋友一定很熟悉這段代碼):
template &
struct fact
{
static const int value = N * fact&::value;
};template &<&>
struct fact&<0&>
{
static const int value = 1;
};
然而,有了constexpr之後,可以用模板函數來計算靜態值了:
template&
constexpr int fact()
{
return X * fact&();
}template &<&>
constexpr int fact&<0&>()
{
return 1;
}
- if constexpr
以往的模板函數的分支只能靠標籤分配:
template &
constexpr auto f()
{
return g(Is_Some_Type&::type());
}constexpr auto g(True_Type)
{
//do something1;
}constexpr auto g(Fale_Type)
{
//do something2;
}
if constexpr大大簡化了編譯期的分支流程編寫。if constexpr編譯期判決,執行其中一個分支,所以另一個分支即使有語義錯誤也沒關係。所以if constexpr基本上可以代替標籤分配(但是這個語句只能用在模板函數中,經典模板元編程用不上這個):
template &
constexpr auto f()
{
if constexpr(Is_Some_Type&::value)
{
//do something1;
}
else
{
//do something2;
}
}
- fold
對於模板的可變參數列表,要想針對每一個類型進行處理,最後得到一個結果,以往的方法是遞歸:
template &
struct SumSquare
{
using type = SumSquare&;
static constexpr int value = (Arg * Arg) + (SumSquare&::value);
};template &
struct SumSquare&
{
using type = SumSquare&;
static constexpr int value = Arg * Arg;
};
然而有了fold之後:
template &
struct SumSquare
{
using type = SumSquare&;
static constexpr int value = (... + (Args * Args));
};
- typename
typename可以用作模板的模板參數了。以前的情況是只能用class聲明,很突兀:
template &
struct Container {}; template & class Con&>
struct S {};
這也算一種進步吧。我一直堅持模板參數寫typename,因為很有現代感。以後就typename就完事了,不用考慮那麼多(當然,經典模板元編程都是傳遞元函數類的,很少直接傳遞元函數):
template &
struct Container {}; template & typename Con&>
struct S {};
C++的模板template一開始並非有意就設計為如此功能龐雜強大,其圖靈完備是後面一不小心挖掘出來,這種非語言設計者預料的重要語言特性,真是很讓人浮想聯翩。C++裡面到底還隱藏有多少圖靈完備的好東西。然後呢,template一般只是用來寫庫代碼,業務層代碼盡量不要出現template的關鍵字,應用層頂多就用用尖括弧。一旦出現template的字眼,就意味著模板庫存在抽象泄露。
對於C++的模板元編程(Template meta program TMP)來說,怎麼做並不重要,重要的是TMP能做什麼,什麼地方該用TMP,能做到什麼效果,其表達能力的局限,這種不足可否通過宏或者反射又或者別的什麼東西來彌補。一般來說,確定了What和Why之後,How就自然而然也有答案了。
可以說,C++的TMP,除了在搞複雜功能的DSL時顯得有點捉襟見肘,這種場合一般出現在用C++生成另外一種圖靈完備語言(好比JavaScript,好比lua),又或者是要用C++做類似於Haskell的monad的do應用,這些例子,其實都是同樣的本質問題,受限於C++先天語法形式的限制。任何其他的一切需求,只要猿猴能想到的語言特性,大C艹都可以實現。任何代碼中存在的不管是何形式的重複(代碼類似、模塊結構類似、設計框架類似,二進位結果重複),都可以通過TMP來予以消除,並且保持代碼的彈性,運行速度塊,佔用內存小等等一系列的優點。好比java的IO設計體系,由於語言抽象能力不行,導致出現幾十個類,還感覺功能不夠完備,還使用不方便。這個在大C艹中,只需幾個類的靈活組合,就可以搭配出來java龐大IO體系的完整功能。
好比qsort,我實在太喜歡拿這個函數來說事了,因為它的確可以說明很多問題。void qsort(void*base,size_t num,size_t width,int(__cdecl*compare)(const void*,const void*)),前面兩個參數都沒什麼,必須的,後面的兩個參數就很討厭,特別是最後的函數指針,就更讓人心煩意亂了。我們來包裝這個函數,讓其用起來更清爽,通過類型推導,調用時,只需傳遞前面兩個參數即可。
using StdCompareFunc = int(*)(const void* x, const void* y);
template&
struct TComparor
{
static int Callback(const Ty x, const Ty y)
{
return x &< y ? -1 : x == y ? 0 : 1; } static StdCompareFunc Apply() { return reinterpret_cast&(Callback);
}
};template&
void QSort(Ty* values, size_t count)
{
qsort(values, count, sizeof(Ty), TComparor&::Apply());
}
希望第四個參數通過類型參數,可以把它推導出來,這時候,自然要請出TMP這個大殺器了。首先,先寫一個模板類TComparor,用以提供兩個元素之間的比較操作。後面我們將看到,隨著需求的複雜性,這個類的設計將會引入越來越多的概念。但是目前來看,它很好理解,雖然簡單,但是還具備一點點彈性,請看。
比如,對於用戶自定義類型,假如它不重載小於號&<和==等於號這兩個重載函數,必然將編譯不過通過,編譯器可能會吐出上噸的錯誤或者告警信息,以宣洩不滿。
對此,有兩種應付的方法,其一,重載這個比較函數就是。其次,針對此自定義類型,全特化TComparor。好比如下所示
struct MyType {...};
template&<&>
struct TComparor&
{
static int Callback(const MyType x, const MyType y)
{
return ...;
}static StdCompareFunc Apply()
{
return reinterpret_cast&(Callback);
}
};
代碼看起來有點冗長,寫起來煩,更令人不爽的是這種特化還必須寫在TComparor的命名空間裡面,而非自定義類型的自己空間,這種規定雖然語法設計上必須這樣,但是的確代碼噪音太多。不如我們約定一下,只要自定義類型擁有CompareTo的成員函數,那麼它就可以用於QSort函數中。因此,TComparor的Callback函數就要做出相應的修改,先嘗試調用Ty裡面的成員函數CompareTo,調用失敗,則回到原來的處理邏輯。所以,我們馬上就面臨在靜態編譯時檢查Ty裡面是否存在CompareTo成員函數這個問題了。這樣的分支判斷寫法,在C++11以前,要寫大片大片的代碼,但是modern C++手裡,有了很大的好轉,如果可以用C++17,那做起來就更快了。請看大屏幕
namespace Detail
{
#define PPVal(...) std::declval&<__VA_ARGS__&>()template&
struct ZImpCompare
{
static int Apply(const Ty x, const Ty y)
{
return x &< y ? -1 : x == y ? 0 : 1; } }; template&
struct ZImpCompare&
{
static int Apply(const Ty x, const Ty y)
{
return x.CompareTo(y);
}
};
}template&
struct TComparor
{
static int Callback(const Ty x, const Ty y){return Detail::ZImpCompare&::Apply(x, y);}
...
};
寫到這裡,我們突然發現,只要再小小地努力一把,QSort似乎還能快排元素為指針的數組。只需加入以下的部分特化代碼,馬上就可以實現這個大功能。
template&
struct TComparor&
{
static int Callback(const Ty* x, const Ty* y)
{
return Detail::ZImpCompare&::Apply(*x, *y);
}static StdCompareFunc Apply()
{
return reinterpret_cast&(Callback);
}
};
只此,通過TMP的手法,我們拓展了qsort的功能,讓其用起來又方便又不容易出錯(靜態類型安全),而且性能上還沒有一點點的小損失。
按:還可以在QSort上搞以成員欄位為依據的元素比較操作。以上只是玩具代碼,工業級運用時,考慮到一大坨情況,代碼會複雜很多。但是,不可否認,絕大多數情況下(對於極少數例外,自行調用qsort就是,這沒什麼不好的),QSort已經比qsort要好用很多,並且,還無須付出任何代價,真的,不管是內存佔用,運行時間,還是二進位大小,都沒有一丁點的付出。可能很多人不爽QSort的這種實現方式,但是,沒關係,一開始我們的目標就只是改進qsort的易用性而已,只要能做到這一點就心滿意足了,而通過以上的操作,的確實現了這個需求,其他的,who care?簡單來說,QSort就是qsort好用,又不需要付出任何代價,這不就行了。至於QSort本身有多不好,那已經不是我們要顧慮的事情。寫C++代碼最怕糾結,一糾結,什麼都做不了啦,因為裡面存在太多太多的誘惑,只要你高興,猿猴可以做無窮無盡的優化,不會有盡頭。
這就是C++模板的體貼之處,不管底層庫背地裡做了多少見不得人的勾當,應用層的代碼都感受不到,template是不必用的,頂多就用一下尖括弧,有時候連尖括弧都不必用。
為了模版元編程,我搞了個編程語言Birdee。可以在代碼里內嵌python腳本,實現編譯期的元編程。基本語法類似vb,可以與C和C++相互調用,一同鏈接
基hub地址 http://github.com/Birdee-lang/Birdee2
這是我和小夥伴一起寫的編程語言,靜態編譯,後端LLVM生成代碼,可以與C/C++一同鏈接。效率和簡易度可以認為是介於C++和java之間。支持垃圾回收,模版,lambda表達式等特性。
核心功能是支持在代碼中嵌入Python代碼,來替代C中的宏還有模版元編程功能。由於C++模版編程複雜,難以學習,所以我們的語言引入python腳本,腳本的表達能力比模版元編程更好更清晰。Python代碼是在編譯時運行的,不影響運行時效率。還有語言引入「註解」,可以通過python腳本來修改現有的代碼AST,用於編寫編譯器插件。
還有整個編譯器可以作為python的庫被python直接調用,生成Birdee代碼
C++11的type_traits頭文件,裡面的traits有些很實用。
不過說實話enable_if還是太醜陋了一點,所以C++17有了void_t。不過說實話void_t還是太醜陋了一點,所以C++2a增加了對concept的語法支持。C++11的參數包給了模板元更大的潛力和可能性。
不過說實話功能太弱了,寫起來巨麻煩。於是……C++17引入摺疊表達式,可以直接在運算中展開參數包。
總比沒有好。別名模板(using)可以讓代碼更清爽。最方便的寫法就是(對比較複雜的代碼生成)別用元編程,老老實實用別的腳本語言+配置文件寫真實的代碼生成。有如下重大收益:
- 編譯bug不會刷屏。
- 避免模板元暴露在頭文件的問題,提升編譯速度。
- 調試生成器腳本+生成出來的代碼,比直接調試一坨模板元要容易,反正我覺得是這樣。
另外,對於無所謂的運行時多態,就不要企圖推到編譯時。為微不足道的運行時效率提升而大幅度犧牲開發效率、代碼易讀性,是不值當的。
c++的 concept 發展起來,模板元編程才能被稱之為完善方便,目前的各種模板元編程手法更像是東拼西湊,打補丁。
用函數重載,不要用特化
template&
true_type is_same(T, T);template&
false_type is_same(T1, T2);
用的時候decltype
C++11的expression SFINAE,和基於此上的detection idiom,配合C++17的constexpr if和C++2a的Concept支持對於模板元編程如虎添翼.
當然還有最重要的varadic templates,直接讓很多不可行的東西變成可行了.
嗯…enable_if_t這種算不算?感覺方便了很多。
推薦閱讀: