C++模板技法收錄

C++ 模板技法

C++模板的學習,一來就是Trick太多,二來是規則太複雜並且資料少。我但願在這裏總結一下,方便學習。這些C++特性可能只能在比較新的編譯器上才能正確編譯。下面的代碼也都只是Demo,萬不能在生產環境中使用。應用在生產環境中時,你還要考慮到const的增減、右值引用以優化性能、訪問控制以加強封裝等等。程序員

此文不按期更新。若這些代碼在你的編譯器上沒法編譯成功,請及時告知。express

1、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裏面已經用到爛了。學習

2、 SFINAE: 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習慣性落後於標準的思想。

3、 CRTP: 線性代數類實做

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 ] ]
}
相關文章
相關標籤/搜索