C++模板的學習,一來就是Trick太多,二來是規則太複雜並且資料少。我但願在這裏總結一下,方便學習。這些C++特性可能只能在比較新的編譯器上才能正確編譯。下面的代碼也都只是Demo,萬不能在生產環境中使用。應用在生產環境中時,你還要考慮到const
的增減、右值引用以優化性能、訪問控制以加強封裝等等。程序員
此文不按期更新。若這些代碼在你的編譯器上沒法編譯成功,請及時告知。express
type_traits
用一個空類Helper,以下面的integral_trait
,將全部所需類型的相關內容包裝起來(你可能想問我爲何要用enum
,答案是隻是想少寫點字而已,尤爲在你有幾個整數要填充進Helper裏的時候。固然你也能夠寫static const bool is_integral = false;
,不過這實在是太長了):數組
template<typename T> struct integral_trait { enum { is_integral = false, }; }; template<> struct integral_trait<char> { enum { is_integral = true, }; }; template<> struct integral_trait<unsigned char> { enum { is_integral = true, }; }; template<> struct integral_trait<int> { enum { is_integral = true, }; }; template<> struct integral_trait<unsigned int> { enum { is_integral = true, }; }; template<typename T, typename Trait = integral_trait<T>> bool is_integral_number(T) { return Trait::is_integral; } //////////////////////////////////////////////////////////// // My classes struct big_integer { big_integer(const string&) { } }; template<> struct integral_trait<big_integer> { enum { is_integral = true, }; }; int main() { cout << is_integral_number(123) << endl; // 1 cout << is_integral_number(123.) << endl; // 0 cout << is_integral_number(big_integer("123")) << endl; // 1 cout << is_integral_number("123") << endl; // 0 }
這樣有什麼好處呢?當你有一個本身的新類(如big_integer
),你想讓它可以適配is_integral_number
,讓這個函數認爲它是個整數,你只須要爲你的類添加一個integral_trait
就行了。函數
Trait Helper在充當適配器的功能下幾乎無所不能。你能爲了能讓數組和模板容器用上統一的迭代接口,用一個trait去把二者的區別磨平。性能
template<typename T> struct iterator_trait { typedef typename T::iterator iterator; static iterator begin(T& c) { return c.begin(); } static iterator end(T& c) { return c.end(); } }; template<typename T, size_t N> struct iterator_trait<T[N]> { typedef T* iterator; static iterator begin(T arr[N]) { return arr; } static iterator end(T arr[N]) { return arr + N; } }; template<typename T, typename Trait = iterator_trait<T>> // use reference to keep array from decaying to pointer void print_each(T& container) { for(typename Trait::iterator i = Trait::begin(container); i != Trait::end(container); ++i) cout << *i << ' '; cout << endl; } int main() { int arr_i[] = { 1, 2 }; string arr_s[] = { "3", "4" }; vector<float> vec_f = { 5.f, 6.f }; list<double> lst_d = { 7., 8. }; print_each(arr_i); // 1 2 print_each(arr_s); // 3 4 print_each(vec_f); // 5 6 print_each(lst_d); // 7 8 }
Trait技法在STL裏面已經用到爛了。學習
enable_if
Substitution failure is not an error (SFINAE),替代失敗不是錯誤,是一項C++98開始有的特性,意思是編譯器在模板推導的時候,將全部可能可用的聲明組合列出來,找到可用的一組(有且僅有一組,留意到若是可用的不止一個,編譯器會報歧義錯誤),其他不可用的組合不會報錯。最經典的應用是enable_if
,它的實現是這樣的:測試
template<bool B, class T = void> struct enable_if {}; template<class T> struct enable_if<true, T> { typedef T type; };
咱們先解釋下用法。好比在調用what_am_i(123);
時,編譯器把幾個可能的推導列出來。除了第一個函數之外,其它全部都在..., int>::type
的時候出錯了,由於當B != true
的時候,enable_if
沒法被偏特化,因此也就是沒有enable_if::type
了。優化
template<typename T, typename enable_if<is_integral<T>::value, int>::type n = 0> void what_am_i(T) { cout << "Integral number." << endl; } template<typename T, typename enable_if<is_floating_point<T>::value, int>::type n = 0> void what_am_i(T) { cout << "Floating point number." << endl; } template<typename T, typename enable_if<is_pointer<T>::value, int>::type n = 0> void what_am_i(T) { cout << "Pointer." << endl; } int main() { what_am_i(123); what_am_i(123.0); what_am_i("123"); }
天哪太醜了!你不由叫了出來,心情跟你看到重載後置operator++
的時候要加多一個參數(而且它毫無做用的時候)同樣。爲何非要在後面加一個毫無做用的模板參數啊!enable_if
最麻煩的問題就是,typename enable_if<is_???<T>::value, ???>::type
該怎麼放以及放哪兒。我來列出幾個可能的地點,而後逐一排除:this
首先,我先明確咱們的目的:誘導在函數裏產生類型推導的失敗,讓具備SFINAE特性的編譯器把它排除。只要出現失敗就好了,有無心義什麼的……咱們不關心。設計
函數體裏面:說出這種話的人是白癡嗎,編譯器在推導類型的時候纔不會關心函數體裏面是怎樣的呢。若是將失敗置放在函數體裏面,那就不是匹配失敗了,而是真正的編譯器錯誤。
做爲函數參數類型:
template<typename T> void what_am_i(typename enable_if<is_pointer<T>::value, T>::type) { /* ... */ }
不能夠。what_am_i(123)
是在告訴編譯器你想經過一系列關於T的的表達式獲得一個int,而後讓編譯器把T像解方程同樣解出來 —— 編譯器可不是MATLAB。除非你這樣寫:what_am_i<int>(123)
,編譯器纔有這個能力把匹配作對。
返回值:
template<typename T> typename enable_if<is_pointer<T>::value, void>::type what_am_i(T) { /* ... */ }
是的你能夠這樣作,並且這樣作很漂亮。這也是正確寫法之一。
做爲模板參數的默認值:
template<typename T, typename t = typename enable_if<is_floating_point<T>::value, T>::type> void what_am_i(T) { /* ... */ }
若是你只有一個what_am_i
的話,這是可行的。然而當咱們有多個,就會引發歧義了。模板參數像函數參數同樣,有一個簽名,若是你全部的what_am_i
都是這樣作的,這樣每一個函數的函數簽名都是是template<typename T, typename> what_am_i(T)
,根本沒辦法引發重載。類比一下,試想你聲明兩個函數int add(int a, int b = 0)
和int add(int a, int b = 1)
,編譯器可不會認爲這兩個函數相互重載了,只是默認值相同的話。
做爲模板參數:
template<typename T, typename enable_if<is_floating_point<T>::value, T>::type = 0> void what_am_i(T) { /* ... */ }
咱們快要接近正確答案了。可是這個仍然不行。關鍵出在T上:用int是別有用意的。能做爲非類型模板(Non-type Template)的類型幾乎只有整數類型,浮點不行,類/結構不行,部分指針是能夠的。緣由是這些浮點啊、類啊它們的「相等」概念很是含糊(好比浮點陷阱),編譯器沒法做出匹配。因此,明智的辦法是用整數吧。
因此enable_if
是精心設計的出來的。做者雖只用了短短几行,,背後卻幾乎涵蓋了整個C++模板推導的機制。
在C++11裏面增添了一個decltype
關鍵字,用在函數聲明有這樣的寫法:
template<typename T> auto func(T t1, T t2) -> decltype(t1+t2);
你如今能夠令decltype裏的表達式發生錯誤以跳過這個匹配(例如T是一個指針,指針不能相加,表達式t1+t2
是ill-form expression),可是在編寫程序的時候儘可能避免,由於Visual Studio到如今仍未完整支持decltype裏的SFINAE(又叫Expression SFINAE),完美傳承了Visual Studio習慣性落後於標準的思想。
CRTP(Curiously recurring template pattern),奇異遞歸模板模式,是一個頗有用的自我擴充的方法。在STL裏面,有一個很經典的應用就是 enable_shared_from_this
,不過動態內存管理的內容這裏不詳談。
所謂CRTP,就是諸如template<class T> class A : B<T> { ... };
這種形式。在類的定義還未徹底出來以前,就繼承自以本身爲模板的另一個類。編譯器對這種狀況採用相似與模板特化的惰性推演:在未將全部的模板參數實參化以前,它還不是一個真真正正的類型;而當填充了實參以後,這個類的定義也就早就出來了,因此也就沒有相似於"incomplete type"的錯誤 —— 真是很聰明的設計,有一種超越時間空間的存在的感受(笑
事情是這樣的,一個程序員在實現線性代數庫的時候都快要瘋掉了。他要實現三種類型:複數、向量、矩陣,他們的操做幾乎如出一轍。然而你沒辦法讓它們從某個基類繼承過來,由於一來從數學上說不通,二來方式也是有所區別的。它們都有加法運算,簡直如出一轍的加法運算。它們都有乘法運算,天差地別的乘法運算。並且只要知足一些規定(好比列向量的行數等於左矩陣的列數),這些類型二者之間也能夠互相相乘。更麻煩的是,你還要在實現完 operator+
, operator*
以後,實現operator+=
和operator*=
,還有知足交換律的二者對調。
這些功能均可以經過CRPT來整合到當前的類裏面,而且經過Specialization來爲不一樣的類型實現相同的接口。下面咱們來看看這些線性代數類是怎樣實現的。首先,我但願咱們這些結構均可以有加法,能夠格式化打印出來:
template<typename T> struct add_impl { static T add(T l, const T& r) { typedef decltype(*r.begin()) val; transform(r.begin(), r.end(), l.begin(), l.begin(), [](const val& vl, const val& rl) { return vl + rl; }); return l; } }; template<typename Base, template<typename> class Impl> struct add_ops { Base& self; add_ops(Base& s) : self(s) { } template<typename T1> auto operator+(const T1& other) const -> decltype(Impl<Base>::add(self, other)) { return Impl<Base>::add(self, other); } template<typename T1> auto operator+=(const T1& other) -> decltype(self) { return (self = operator+(other)); } };
在這裏,將做爲(見下文的三個主要的類)父類的add_ops
沒有實現加法操做,而是把它轉發給一個模板Impl。這個Impl多是add_impl
或者任何一個擁有一個模板參數,而且實現了add
語義的類。這裏用到了一種技法叫作Template template parameter,模板模板參數,用來約束模板參數Impl也必須擁有一個模板參數。若是不用TTP,在生成add_ops
的時候就不能用add_ops<Base, add_impl>
而必須用add_ops<Base, add_impl<Base>>
。
矩陣的格式化打印跟向量的格式化打印是相似的,都是[ ... ]
,可是複數咱們但願可以顯示成a+bi
的形式。因此我經過模板特化來爲複數增長特性:
template<typename T> struct fmt_impl { static void fmt(ostream& os, const T& t) { typedef decltype(*t.begin()) val; os << "[ "; for_each(t.begin(), t.end(), [&](const val& v) { os << v << ' '; }); os << "]"; } }; template<typename T> class cmplx; template<typename T> struct fmt_impl<cmplx<T>> { static void fmt(ostream& os, const cmplx<T>& c) { os << c[0] << "+" << c[1] << "i"; } }; template<typename Base, template<typename> class Impl> struct fmt_ops { Base& self; fmt_ops(Base& s) : self(s) { } }; template<typename Base, template<typename> class Impl> inline ostream& operator<<(ostream& os, const fmt_ops<Base, Impl> ops) { Impl<Base>::fmt(os, ops.self); return os; }
說了這麼久主人公還沒現身。在實現了上面幾個模板類以後,咱們所需的複數、向量、矩陣三個類就輕鬆了,除了基本的構造函數和operator=
須要自行實現外,只須要用CRPT技法繼承自???_ops
來歸入新的方法就好。爲了省事我決定把三個類都繼承自std::array
來獲取諸如迭代器、下標操做等功能,固然我也能夠用相似的方法用CRPT實現它,不過限於篇幅就不這麼作了:
template<typename T> struct cmplx : public array<T, 2>, public add_ops<cmplx<T>, add_impl>, public fmt_ops<cmplx<T>, fmt_impl> { typedef array<T, 2> array_t; typedef add_ops<cmplx<T>, add_impl> add_ops_t; typedef fmt_ops<cmplx<T>, fmt_impl> fmt_ops_t; cmplx() : add_ops_t(*this), fmt_ops_t(*this) { } cmplx(const cmplx& c) : array_t(c), add_ops_t(*this), fmt_ops_t(*this) { } cmplx(initializer_list<T> l) : add_ops_t(*this), fmt_ops_t(*this) { copy_n(l.begin(), 2, this->begin()); } cmplx& operator=(const cmplx& c) { array_t::operator=(c); return *this; } }; template<typename T, size_t N> struct vec : public array<T, N>, public add_ops<vec<T, N>, add_impl>, public fmt_ops<vec<T, N>, fmt_impl> { typedef array<T, N> array_t; typedef add_ops<vec<T, N>, add_impl> add_ops_t; typedef fmt_ops<vec<T, N>, fmt_impl> fmt_ops_t; vec() : add_ops_t(*this), fmt_ops_t(*this) { } vec(const vec& v) : array_t(v), add_ops_t(*this), fmt_ops_t(*this) { } vec(initializer_list<T> l) : add_ops_t(*this), fmt_ops_t(*this) { copy_n(l.begin(), N, this->begin()); } vec& operator=(const vec& v) { array_t::operator=(v); return *this; } }; template<typename T, size_t N, size_t M> struct mat : public array<vec<T, N>, M>, public add_ops<mat<T, N, M>, add_impl>, public fmt_ops<mat<T, N, M>, fmt_impl> { typedef array<vec<T, N>, M> array_t; typedef add_ops<mat<T, N, M>, add_impl> add_ops_t; typedef fmt_ops<mat<T, N, M>, fmt_impl> fmt_ops_t; mat() : add_ops_t(*this), fmt_ops_t(*this) { } mat(const mat& m) : array_t(m), add_ops_t(*this), fmt_ops_t(*this) { } mat(initializer_list<vec<T, N>> l) : add_ops_t(*this), fmt_ops_t(*this) { copy_n(l.begin(), M, this->begin()); } mat& operator=(const mat& m) { array_t::operator=(m); return *this; } }; typedef cmplx<double> c; typedef vec<double, 3> vec3; typedef vec<c, 3> vec3c; typedef mat<double, 3, 2> mat32;
代碼變得很是簡短。三個類都僅用了十多行代碼就含有了咱們所需的功能:加法、格式化輸出。想要加入乘法等功能也能夠採用相似的手段。咱們測試一下這些代碼以保證它是可行的:
int main() { vec3 v1{1, 2, 3}; vec3 v2{4, 5, 6}; vec3 v3 = v1 + v2; cout << v3 << endl; // [ 5 7 9 ] vec3c vc1{c{0, 1}, c{1, 2}, c{2, 3}}; vec3c vc2{c{4, 5}, c{5, 6}, c{6, 7}}; vec3c vc3 = vc1 + vc2; cout << vc3 << endl; // [ 4+6i 6+8i 8+10i ] vc3 += vc1; cout << vc3 << endl; // [ 4+7i 7+10i 10+13i ] mat32 m1{ vec3{1, 2, 3}, vec3{4, 5, 6}, }; mat32 m2{ vec3{4, 5, 6}, vec3{7, 8, 9}, }; mat32 m3 = m2 + m1; cout << m3 << endl; // [ [ 5 7 9 ] [ 11 13 15 ] ] }