C++模板技法收录
C++ 模板技法
C++模板的学习,一来就是Trick太多,二来是规则太复杂而且资料少。我希望在这里总结一下,方便学习。这些C++特性可能只能在比较新的编译器上才能正确编译。下面的代码也都只是Demo,万不能在生产环境中使用。应用在生产环境中时,你还要考虑到const
的增减、右值引用以优化性能、访问控制以增强封装等等。
此文不定期更新。若这些代码在你的编译器上无法编译成功,请及时告知。
一、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 classesstruct 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 pointervoid 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里面已经用到烂了。
SFINAE: enable_if
Substitution failure is not an error (SFINAE),替代失败不是错误,是一项新的C++特性,Visual Studio从2010开始支持,意思是编译器在模板推导的时候,将所有可能可用的声明组合列出来,找到可用的一组(有且仅有一组,留意到如果可用的不止一个,编译器会报歧义错误),其余不可用的组合不会报错。最经典的应用是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
该怎么放以及放哪儿。我来列出几个可能的地点,然后逐一排除:
首先,我先明确我们的目的:诱导在函数里产生类型推导的失败,让具有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, T>::type what_am_i(T){ /* ... */ }
是的你可以这样做,而且这样做很漂亮。可惜我们希望返回值是
void
。作为模板参数的默认值:
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++模板推导的机制。
CRTP: 线性代数类实作
CRTP(Curiously recurring template pattern),奇异递归模板模式,是一个很有用的自我扩充的方法。在STL里面,有一个很经典的应用就是 enable_shared_from_this
,不过动态内存管理的内容这里不详谈。
所谓CRTP,就是诸如template<class T> class A : B<T> { ... };
这种形式。在类的定义还未完全出来之前,就继承自以自己为模板的另外一个类。编译器对这种情况采用类似与偏特化的惰性推演:在未将所有的模板参数实参化之前,它还不是一个真真正正的类型;而当填充了实参之后,这个类的定义也就早就出来了,所以也就没有类似于"incomplete type"的错误 —— 真是很聪明的设计,有一种超越时间空间的存在的感觉(笑,C++编译器就是元编程时用的的脚本解释器对吧)。
事情是这样的,一个程序员在实现线性代数库的时候都快要疯掉了。他要实现三种类型:复数、向量、矩阵,他们的操作几乎一模一样。然而你没办法让它们从某个基类继承过来,因为一来从数学上说不通,二来方式也是有所区别的。它们都有加法运算,简直一模一样的加法运算。它们都有乘法运算,天差地别的乘法运算。而且只要满足一些规定(比如列向量的行数等于左矩阵的列数),这些类型两者之间也可以互相相乘。更麻烦的是,你还要在实现完 operator+
, operator*
之后,实现operator+=
和operator*=
,还有满足交换律的两者对调。
这些功能都可以通过CRPT来整合到当前的类里面,并且通过Partial 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 ] ]}