上面代码函数实现写在类的内部,即内联,如果编译器对内联支持的好的话,上面代码几乎等价于如下代码: #include <iostream> // std::cout #include <cmath> // std::sqrt() void evaluate(double start, double end, double step) { double _temp = 1.0; for(double i=start; i<end; i+=step) std::cout << -i / std::sqrt(_temp + i) << ' '; } int main() { evaluate(0.0, 10.0, 1.0); std::cin.get(); return 0; } -0 -0.707107 -1.1547 -1.5 -1.78885 -2.04124 -2.26779 -2.47487 -2.66667 -2.84605 和表达式模板类似的技术还可以用到向量计算中,以避免产生临时向量变量,见文献[4] Expression templates 和文献[12]的后面。传统向量计算如下: class DoubleVec; // DoubleVec 重载了 + - * / 等向量元素之间的计算 DoubleVec y(1000), a(1000), b(1000), c(1000), d(1000); // 向量长度 1000 // 向量计算 y = (a + b) / (c - d); // 等价于 DoubleVec __t1 = a + b; DoubleVec __t2 = c - d; DoubleVec __t3 = __t1 / __t2; y = __t3; 模板代码实现向量计算如下: template<class A> DVExpr; class DVec{ // ... template<class A> DVec& operator=(const DVExpr<A>&); // 由 = 引起向量逐个元素的表达式值计算并赋值 }; DVec y(1000), a(1000), b(1000), c(1000), d(1000); // 向量长度 1000 // 向量计算 y = (a + b) / (c - d); // 等价于 for(int i=0; i<1000; ++i) { y[i] = (a[i] + b[i]) / (c[i] + d[i]); } 不过值得一提的是,传统代码可以用 C++11 的右值引用提升性能,C++11 新特性我们以后再详细讨论。 我们这里看下文献[4] Expression templates 实现的版本,它用到了编译期多态,编译期多态示意代码如下(关于这种代码形式有个名字叫 curiously recurring template pattern, CRTP,见文献[4]): // 模板基类,定义接口,具体实现由模板参数,即子类实现 template <typename D> class base { public: void f1() { static_cast<E&>(*this).f1(); } // 直接调用子类实现 int f2() const { static_cast<const E&>(*this).f1(); } }; // 子类 class dirived1 : public base<dirived1> { public: void f1() { /* ... */ } int f2() const { /* ... */ } }; template<typename T> class dirived2 : public base<dirived2<T>> { public: void f1() { /* ... */ } int f2() const { /* ... */ } }; 简化后(向量长度固定为1000,元素类型为 double)的向量计算代码如下: #include <iostream> // std::cout // A CRTP base class for Vecs with a size and indexing: template <typename E> class VecExpr { public: double operator[](int i) const { return static_cast<E const&>(*this)[i]; } operator E const&() const { return static_cast<const E&>(*this); } // 向下类型转换 }; // The actual Vec class: class Vec : public VecExpr<Vec> { double _data[1000]; public: double& operator[](int i) { return _data[i]; } double operator[](int i) const { return _data[i]; } template <typename E> Vec const& operator=(VecExpr<E> const& vec) { E const& v = vec; for (int i = 0; i<1000; ++i) _data[i] = v[i]; return *this; } // Constructors Vec() { } Vec(double v) { for(int i=0; i<1000; ++i) _data[i] = v; } }; template <typename E1, typename E2> class VecDifference : public VecExpr<VecDifference<E1, E2> > { E1 const& _u; E2 const& _v; public: VecDifference(VecExpr<E1> const& u, VecExpr<E2> const& v) : _u(u), _v(v) { } double operator[](int i) const { return _u[i] - _v[i]; } }; template <typename E> class VecScaled : public VecExpr<VecScaled<E> > { double _alpha; E const& _v; public: VecScaled(double alpha, VecExpr<E> const& v) : _alpha(alpha), _v(v) { } double operator[](int i) const { return _alpha * _v[i]; } }; // Now we can overload operators: template <typename E1, typename E2> VecDifference<E1, E2> const operator-(VecExpr<E1> const& u, VecExpr<E2> const& v) { return VecDifference<E1, E2>(u, v); } template <typename E> VecScaled<E> const operator*(double alpha, VecExpr<E> const& v) { return VecScaled<E>(alpha, v); } int main() { Vec u(3), v(1); double alpha=9; Vec y; y = alpha*(u - v); std::cout << y[999] << '\n'; std::cin.get(); return 0; } 18 “alpha*(u – v)” 的类型推断过程如下图所示,其中有子类到基类的隐式类型转换: 这里可以看到基类的作用:提供统一的接口,让 operator- 和 operator* 可以写成统一的模板形式。
7. 特性,策略,标签 利用迭代器,我们可以实现很多通用算法,迭代器在容器与算法之间搭建了一座桥梁。求和函数模板如下: #include <iostream> // std::cout #include <vector> template<typename iter> typename iter::value_type mysum(iter begin, iter end) { typename iter::value_type sum(0); for(iter i=begin; i!=end; ++i) sum += *i; return sum; } int main() { std::vector<int> v; for(int i = 0; i<100; ++i) v.push_back(i); std::cout << mysum(v.begin(), v.end()) << '\n'; std::cin.get(); return 0; } 4950 我们想让 mysum() 对指针参数也能工作,毕竟迭代器就是模拟指针,但指针没有嵌套类型 value_type,可以定义 mysum() 对指针类型的特例,但更好的办法是在函数参数和 value_type 之间多加一层 — 特性(traits)(参考了文献[1]第72页,特性详见文献[1] 12.1): // 特性,traits template<typename iter> class mytraits{ public: typedef typename iter::value_type value_type; }; template<typename T> class mytraits<T*>{ public: typedef T value_type; }; template<typename iter> typename mytraits<iter>::value_type mysum(iter begin, iter end) { typename mytraits<iter>::value_type sum(0); for(iter i=begin; i!=end; ++i) sum += *i; return sum; } int main() { int v[4] = {1,2,3,4}; std::cout << mysum(v, v+4) << '\n'; std::cin.get(); return 0; } 10 其实,C++ 标准定义了类似的 traits:std::iterator_trait(另一个经典例子是 std::numeric_limits) 。特性对类型的信息(如 value_type、 reference)进行包装,使得上层代码可以以统一的接口访问这些信息。C++ 模板元编程会涉及大量的类型计算,很多时候要提取类型的信息(typedef、 常量值等),如果这些类型的信息的访问方式不一致(如上面的迭代器和指针),我们将不得不定义特例,这会导致大量重复代码的出现(另一种代码膨胀),而通过加一层特性可以很好的解决这一问题。另外,特性不仅可以对类型的信息进行包装,还可以提供更多信息,当然,因为加了一层,也带来复杂性。特性是一种提供元信息的手段。 策略(policy)一般是一个类模板,典型的策略是 STL 容器(如 std::vector<>,完整声明是template<class T, class Alloc=allocator<T>> class vector;)的分配器(这个参数有默认参数,即默认存储策略),策略类将模板的经常变化的那一部分子功能块集中起来作为模板参数,这样模板便可以更为通用,这和特性的思想是类似的(详见文献[1] 12.3)。 标签(tag)一般是一个空类,其作用是作为一个独一无二的类型名字用于标记一些东西,典型的例子是 STL 迭代器的五种类型的名字(input_iterator_tag, output_iterator_tag, forward_iterator_tag, bidirectional_iterator_tag, random_access_iterator_tag),std::vector<int>::iterator::iterator_category 就是 random_access_iterator_tag,可以用第1节判断类型是否等价的模板检测这一点: #include <iostream> #include <vector> template<typename T1, typename T2> // 通例,返回 false class theSameType { public: enum { ret = false }; }; template<typename T> // 特例,两类型相同时返回 true class theSameType<T, T> { public: enum { ret = true }; }; int main(){ std::cout << theSameType< std::vector<int>::iterator::iterator_category, std::random_access_iterator_tag >::ret << '\n'; std::cin.get(); return 0; } 1 有了这样的判断,还可以根据判断结果做更复杂的元编程逻辑(如一个算法以迭代器为参数,根据迭代器标签进行特例化以对某种迭代器特殊处理)。标签还可以用来分辨函数重载,第5节中就用到了这样的标签(recursion)(标签详见文献[1] 12.1)。
8. 更多类型计算 在第1节我们讲类型等价的时候,已经见到了一个可以判断两个类型是否等价的模板,这一节我们给出更多例子,下面是判断一个类型是否可以隐式转换到另一个类型的模板(参考了文献[6] Static interface checking): #include <iostream> // std::cout // whether T could be converted to U template<class T, class U> class ConversionTo { typedef char Type1[1]; // 两种 sizeof 不同的类型 typedef char Type2[2]; static Type1& Test( U ); // 较下面的函数,因为参数取值范围小,优先匹配 static Type2& Test(...); // 变长参数函数,可以匹配任何数量任何类型参数 static T MakeT(); // 返回类型 T,用这个函数而不用 T() 因为 T 可能没有默认构造函数 public: enum { ret = sizeof(Test(MakeT()))==sizeof(Type1) }; // 可以转换时调用返回 Type1 的 Test() }; int main() { std::cout << ConversionTo<int, double>::ret << '\n'; std::cout << ConversionTo<float, int*>::ret << '\n'; std::cout << ConversionTo<const int&, int&>::ret << '\n'; std::cin.get(); return 0; } 1 0 0 下面这个例子检查某个类型是否含有某个嵌套类型定义(参考了文献[4] Substitution failure is not an erro (SFINAE)),这个例子是个内省(反射的一种): #include <iostream> #include <vector> // thanks to Substitution failure is not an erro (SFINAE) template<typename T> struct has_typedef_value_type { typedef char Type1[1]; typedef char Type2[2]; template<typename C> static Type1& test(typename C::value_type*); template<typename> static Type2& test(...); public: static const bool ret = sizeof(test<T>(0)) == sizeof(Type1); // 0 == NULL }; struct foo { typedef float lalala; }; int main() { std::cout << has_typedef_value_type<std::vector<int>>::ret << '\n'; std::cout << has_typedef_value_type<foo>::ret << '\n'; std::cin.get(); return 0; } 1 0 这个例子是有缺陷的,因为不存在引用的指针,所以不用用来检测引用类型定义。可以看到,因为只涉及类型推断,都是编译期的计算,不涉及任何可执行代码,所以类的成员函数根本不需要具体实现。 9. 元容器 文献[1]第 13 章讲了元容器,所谓元容器,就是类似于 std::vector<> 那样的容器,不过它存储的是元数据 — 类型,有了元容器,我们就可以判断某个类型是否属于某个元容器之类的操作。 在讲元容器之前,我们先来看看伪变长参数模板(文献[1] 12.4),一个可以存储小于某个数(例子中为 4 个)的任意个数,任意类型数据的元组(tuple)的例子如下(参考了文献[1] 第 225~227 页): #include <iostream> class null_type {}; // 标签类,标记参数列表末尾 template<typename T0, typename T1, typename T2, typename T3> class type_shift_node { public: typedef T0 data_type; typedef type_shift_node<T1, T2, T3, null_type> next_type; // 参数移位了 static const int num = next_type::num + 1; // 非 null_type 模板参数个数 data_type data; // 本节点数据 next_type next; // 后续所有节点数据 type_shift_node() :data(), next() { } // 构造函数 type_shift_node(T0 const& d0, T1 const& d1, T2 const& d2, T3 const& d3) :data(d0), next(d1, d2, d3, null_type()) { } // next 参数也移位了 }; template<typename T0> // 特例,递归终止 class type_shift_node<T0, null_type, null_type, null_type> { public: typedef T0 data_type; static const int num = 1; data_type data; // 本节点数据 type_shift_node() :data(), next() { } // 构造函数 type_shift_node(T0 const& d0, null_type, null_type, null_type) : data(d0) { } }; // 元组类模板,默认参数 + 嵌套递归 template<typename T0, typename T1=null_type, typename T2=null_type, typename T3=null_type> class my_tuple { public: typedef type_shift_node<T0, T1, T2, T3> tuple_type; static const int num = tuple_type::num; tuple_type t; my_tuple(T0 const& d0=T0(),T1 const& d1=T1(),T2 const& d2=T2(),T3 const& d3=T3()) : t(d0, d1, d2, d3) { } // 构造函数,默认参数 }; // 为方便访问元组数据,定义 get<unsigned>(tuple) 函数模板 template<unsigned i, typename T0, typename T1, typename T2, typename T3> class type_shift_node_traits { public: typedef typename type_shift_node_traits<i-1,T0,T1,T2,T3>::node_type::next_type node_type; typedef typename node_type::data_type data_type; static node_type& get_node(type_shift_node<T0,T1,T2,T3>& node) { return type_shift_node_traits<i-1,T0,T1,T2,T3>::get_node(node).next; } }; template<typename T0, typename T1, typename T2, typename T3> class type_shift_node_traits<0, T0, T1, T2, T3> { public: typedef typename type_shift_node<T0,T1,T2,T3> node_type; typedef typename node_type::data_type data_type; static node_type& get_node(type_shift_node<T0,T1,T2,T3>& node) { return node; } }; template<unsigned i, typename T0, typename T1, typename T2, typename T3> typename type_shift_node_traits<i,T0,T1,T2,T3>::data_type get(my_tuple<T0,T1,T2,T3>& tup) { return type_shift_node_traits<i,T0,T1,T2,T3>::get_node(tup.t).data; } int main(){ typedef my_tuple<int, char, float> tuple3; tuple3 t3(10, 'm', 1.2f); std::cout << t3.t.data << ' ' << t3.t.next.data << ' ' << t3.t.next.next.data << '\n'; std::cout << tuple3::num << '\n'; std::cout << get<2>(t3) << '\n'; // 从 0 开始,不要出现 3,否则将出现不可理解的编译错误 std::cin.get(); return 0; } 10 m 1.2 3 1.2 C++11 引入了变长模板参数,其背后的原理也是模板递归(文献[1]第 230 页)。 利用和上面例子类似的模板参数移位递归的原理,我们可以构造一个存储“类型”的元组,即元容器,其代码如下(和文献[1]第 237 页的例子不同): #include <iostream> // 元容器 template<typename T0=void, typename T1=void, typename T2=void, typename T3=void> class meta_container { public: typedef T0 type; typedef meta_container<T1, T2, T3, void> next_node; // 参数移位了 static const int size = next_node::size + 1; // 非 null_type 模板参数个数 }; template<> // 特例,递归终止 class meta_container<void, void, void, void> { public: typedef void type; static const int size = 0; }; // 访问元容器中的数据 template<typename C, unsigned i> class get { public: static_assert(i<C::size, "get<C,i>: index exceed num"); // C++11 引入静态断言 typedef typename get<C,i-1>::c_type::next_node c_type; typedef typename c_type::type ret_type; }; template<typename C> class get<C, 0> { public: static_assert(0<C::size, "get<C,i>: index exceed num"); // C++11 引入静态断言 typedef C c_type; typedef typename c_type::type ret_type; }; // 在元容器中查找某个类型,找到返回索引,找不到返回 -1 template<typename T1, typename T2> class same_type { public: enum { ret = false }; }; template<typename T> class same_type<T, T> { public: enum { ret = true }; }; template<bool c, typename Then, typename Else> class IF_ { }; template<typename Then, typename Else> class IF_<true, Then, Else> { public: typedef Then reType; }; template<typename Then, typename Else> class IF_<false, Then, Else> { public: typedef Else reType; }; template<typename C, typename T> class find { template<int i> class number { public: static const int ret = i; }; template<typename C, typename T, int i> class find_i { public: static const int ret = IF_< same_type<get<C,i>::ret_type, T>::ret, number<i>, find_i<C,T,i-1> >::reType::ret; }; template<typename C, typename T> class find_i<C, T, -1> { public: static const int ret = -1; }; public: static const int ret = find_i<C, T, C::size-1>::ret; }; int main(){ typedef meta_container<int, int&, const int> mc; int a = 9999; get<mc, 1>::ret_type aref = a; std::cout << mc::size << '\n'; std::cout << aref << '\n'; std::cout << find<mc, const int>::ret << '\n'; std::cout << find<mc, float>::ret << '\n'; std::cin.get(); return 0; } 3 9999 2 -1 上面例子已经实现了存储类型的元容器,和元容器上的查找算法,但还有一个小问题,就是它不能处理模板,编译器对模板的操纵能力远不如对类型的操纵能力强(提示:类模板实例是类型),我们可以一种间接方式实现存储“模板元素”,即用模板的一个代表实例(如全用 int 为参数的实例)来代表这个模板,这样对任意模板实例,只需判断其模板的代表实例是否在容器中即可,这需要进行类型过滤:对任意模板的实例将其替换为指定模板参数的代表实例,类型过滤实例代码如下(参考了文献[1]第 241 页): // 类型过滤,meta_filter 使用时只用一个参数,设置四个模板参数是因为,模板通例的参数列表 // 必须能够包含特例参数列表,后面三个参数设置默认值为 void 或标签模板 template<typename T> class dummy_template_1 {}; template<typename T0, typename T1> class dummy_template_2 {}; template<typename T0, typename T1 = void, template<typename> class tmp_1 = dummy_template_1, template<typename, typename> class tmp_2 = dummy_template_2> class meta_filter { // 通例,不改变类型 public: typedef T0 ret_type; }; // 匹配任何带有一个类型参数模板的实例,将模板实例替换为代表实例 template<template<typename> class tmp_1, typename T> class meta_filter<tmp_1<T>, void, dummy_template_1, dummy_template_2> { public: typedef tmp_1<int> ret_type; }; // 匹配任何带有两个类型参数模板的实例,将模板实例替换为代表实例 template<template<typename, typename> class tmp_2, typename T0, typename T1> class meta_filter<tmp_2<T0, T1>, void, dummy_template_1, dummy_template_2> { public: typedef tmp_2<int, int> ret_type; }; 现在,只需将上面元容器和元容器查找函数修改为:对模板实例将其换为代表实例,即修改 meta_container<> 通例中“typedef T0 type;”语句为“typedef typename meta_filter<T0>::ret_type type;”,修改 find<> 的最后一行中“T”为“typename meta_filter<T>::ret_type”。修改后,下面代码的执行结果是: template<typename, typename> class my_tmp_2; // 自动将 my_tmp_2<float, int> 过滤为 my_tmp_2<int, int> typedef meta_container<int, float, my_tmp_2<float, int>> mc2; // 自动将 my_tmp_2<char, double> 过滤为 my_tmp_2<int, int> std::cout << find<mc2, my_tmp_2<char, double>>::ret << '\n'; // 输出 2 2 10. 总结 博文比较长,总结一下所涉及的东西: C++ 模板包括函数模板和类模板,模板参数形式有:类型、模板型、非类型(整型、指针); 模板的特例化分完全特例化和部分特例化,实例将匹配参数集合最小的特例; 用实例参数替换模板形式参数称为实例化,实例化的结果是产生具体类型(类模板)或函数(函数模板),同一模板实参完全等价将产生等价的实例类型或函数; 模板一般在头文件中定义,可能被包含多次,编译和链接时会消除等价模板实例; template、typename、this 关键字用来消除歧义,避免编译错误或产生不符预期的结果; C++11 对模板引入了新特性:“>>”、函数模板也可以有默认参数、变长模板参数、外部模板实例(extern),并弃用 export template; C++ 模板是图灵完备的,模板编程是函数编程风格,特点是:没有可变的存储、递归,以“<>”为输入,typedef 或静态常量为输出; 编译期数值计算虽然实际意义不大,但可以很好证明 C++ 模板的能力,可以用模板实现类似普通程序中的 if 和 while 语句; 一个实际应用是循环展开,虽然编译器可以自动循环展开,但我们可以让这一切更可控; C++ 模板编程的两个问题是:难调试,会产生冗长且难以阅读的编译错误信息、代码膨胀(源代码膨胀、二进制对象文件膨胀),改进的方法是:增加一些检查代码,让编译器及时报错,使用特性、策略等让模板更通用,可能的话合并一些模板实例(如将代码提出去做成单独模板); 表达式模板和向量计算是另一个可加速程序的例子,它们将计算表达式编码到类型,这是通过模板嵌套参数实现的; 特性,策略,标签是模板编程常用技巧,它们可以是模板变得更加通用; 模板甚至可以获得类型的内部信息(是否有某个 typedef),这是反射中的内省,C++ 在语言层面对反射支持很少(typeid),这不利于模板元编程; 可以用递归实现伪变长参数模板,C++11 变长参数模板背后的原理也是模板递归; 元容器存储元信息(如类型)、类型过滤过滤某些类型,它们是元编程的高级特性。
进一步学习 C++ 确实比较复杂,这可能是因为,虽然 C++ 语言层次比较低,但它却同时可以实现很多高级特性。进一步学习 C++ 模板元编程的途径很多: C++ 标准库的 STL 可能是最好的学习案例,尤其是其容器、迭代器、通用算法、函数类模板等部件,实现机制很巧妙; 另外一个 C++ 库也值得一看,那就是 Boost 库,Boost 的元编程库参考文献[16]; 很推荐《深入实践C++模板编程》这本书,这篇博文大量参考了这本书; wikibooks.org 上有个介绍 C++ 各种编程技巧书:More C++ Idioms,文献[15]; 文献[17]列了 C++ 模板的参考书,共四本; 好多东西,书上讲的比较浅显,而且不全面,有时候直接看 C++ 标准(最新 C++11)可能更为高效,C++ 标准并不是想象中那样难读,C++ 标准委员会网站的 Papers 也很值得看,文献[3]。
参考文献: 深入实践C++模板编程,温宇杰著,2013(到当当网); C++程序设计语言,Bjarne Stroustrup著,裘宗燕译,2002(到当当网); C++标准,ISO/IEC 14882:2003,ISO/IEC 14882:2011(到ISO网站,C++标准委员会); wikipedia.org(C++, 模板, Template metaprogramming, Curiously recurring template pattern, Substitution failure is not an erro (SFINAE), Expression templates, C++11, C++14); What does a call to ‘this->template [somename]‘ do (stackoverflow问答); Advanced C++ Lessons,chapter 6,在线教程,2005(到网站); C++ TUTORIAL – TEMPLATES – 2015,bogotobogo.com 网上教程(到网站); C++ Templates are Turing Complete,Todd L. Veldhuizen,2003(作者网站已经停了,archive.org 保存的版本,archive.org 可能被限制浏览); Metaprogramming in C++,Johannes Koskinen,2004(中科大老师保存的版本); C++ Template Metaprogramming in 15ish Minutes(Stanford 课程 PPT,到网站); Template Metaprograms,Todd Veldhuizen,1995(archive.org 保存 Todd Veldhuizen 主页,可能限制访问,在线 PS 文件转 PDF 文件网站); Expression Templates,Todd Veldhuizen,1995; C++ Templates as Partial Evaluation,Todd Veldhuizen,1999; Erwin Unruh 写的第一个模板元编程程序; wikibooks.org(C++ Programming/Templates/Template Meta-Programming,More C++ Idioms); THE BOOST MPL LIBRARY online docs(到网站); Best introduction to C++ template metaprogramming (stackoverflow问答)。注:参考文献中所给的链接,打开不了的,可以参见我的另一篇博客配置浏览器 Johannes Koskinen 论文,Stanford 课程 PPT,Todd Veldhuizen 论文我网盘保存的副本 - 链接: http://pan.baidu.com/s/1ntJstvF 密码: hogb |