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 ] ]
}
相关文章
相关标签/搜索