C++ template 的最初发展动机很直接:让我们得以建立“类型安全”的容器如vector,list和map.然而当愈多人用上template,他们发现template有能力完成愈多可能的变化。容器当然很好,但泛型编程——写出的代码和其所处理的对象类型彼此独立。STL算法如for_each、find和merge 就是这一类编程的成果,C++ template机制自身是一部完整的图灵机:它可以被用来计算任何可计算的值。于是到导出了模板元编程,创造出了“在C++编译器内执行并与编译完成时停止执行”的程序。
条款41:了解隐式接口和编译期多态
假如我们有一个模板函数:
C++ Code
1
2 3 4 5 6 7 8 |
template <
typename T>
bool judge(T &w) { if(w.size() > 10 && w != valA) return true; else return false; } |
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class A
{ public: A( int s = 10): theSize(s) {} virtual char size() const { return ( char)theSize; } bool operator!= ( int other) { return theSize == other; } private: int theSize; }; template < typename T> bool judge(T &w) { if(w.size() > 10 && w != 10) return true; else return false; } |
甚至,只要让&&返回一个bool的值就行了,对于它的左右操作数,则要求不大。
总之,对于类而言,接口是显示的,以函数的声明为中心,多态是通过虚函数实现的;对于模板而言,它的接口是隐式的,多态则是通过模板的函数重载实现的。
条款42:了解typename 的双重意义
在模板的的声明中,class与typename是没有什么区别的:
C++ Code
1
2 |
template <
typename T> T func1(
const T &);
template < class T> T func2( const T &); |
从属名称和非从属名称。
从属名称是依赖于某个类型的,比如迭代器,它是依赖于你的容器类型的;
而非从属名称就不依赖其他类型,比如int。
有了这两个基本概念之后我们就可以看一下例子了:
假设我们要打印一个容器(里面为)中的第二个元素,那么函数应该是这样:
假设我们要打印一个容器(里面为)中的第二个元素,那么函数应该是这样:
C++ Code
1
2 3 4 5 6 7 8 9 |
template <
typename C>
void print2nd(
const C &container)
{ if(container.size() >= 2) { C::const_iterator iter(container.begin()); //错误代码 ++iter; std::cout << *iter; } } |
其中的C::const_iterator,就依赖与传进去的容器的类型。虽然对于这个例子可以成功,但是还是要强调,对于从属名称(这里是C::const_iterator)最好在前面加上typename ,这样可以确保编译器将它看做一个类型名而不是其他什么的。举一个例子,如果是定义为一个指向迭代器的指针:
C++ Code
1
2 3 4 5 6 7 8 |
if(container.size() >=
2)
{ typename C::const_iterator iter(container.begin()); //正确代码 typename C::const_iterator *pIter = &iter; ++(*pIter); std::cout << *(*pIter) << endl; } |
但是总有例外:typename不能出现在基类列表的嵌套从属类型之前,也不可出现在成员初始化列表中作为类型的限定符。
最后,为了获得迭代器所指的类型,可以通过typename std::iterator_traits<C::const_iterator>::value_type temp(*iter);来实现;而且最好是使用typedef来简化代码:
C++ Code
1
2 |
typedef
typename std::iterator_traits<C::const_iterator>::value_type type;
type tmp = (*iter); |
总之,当声明模板参数时,typename与class可以互换,而在模板中定义从属名称时,则只能使用typename,但是在基类列表和成员初始化列表中,不能使用typename来指明从属名称。
条款43:学习处理模板化基类内的名称
先看程序:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class A
{ public: void funcA() { cout << "funcA" << endl; } }; template< typename T> class UseA { public: void useAFunc() { T a; a.funcA(); } }; template< typename T> class DrivedUseA: public UseA<T> { public: void DrivedUseAFunc() { useAFunc(); } }; int main() { DrivedUseA<A> d; d.DrivedUseAFunc(); return 0; } |
这段程序在vc++6.0和vs2010下是没有问题的,但是在有的编译器(g++,还有一些在线编译器)上会报错。原因是因为当编译器遇见DrivedUseA时,并不知道他继承自什么类,直到基类UseA被实例化以后他才知道。而如果他不知道是通过class A实例化的,自然就不知道基类的函数具体是什么了。有3个方法可以解决这个问题:
1.在基类函数调用动作之前加上this->:this->useAFunc();
2.使用using声明:
2.使用using声明:
C++ Code
1
2 3 4 5 6 7 8 9 10 |
template<
typename T>
class DrivedUseA: public UseA<T> { public: using UseA<T>::useAFunc; void DrivedUseAFunc() { useAFunc(); } }; |
3.明确指出被调函数位于基类中
C++ Code
1
2 3 4 5 6 7 8 9 10 |
template<
typename T>
class DrivedUseA: public UseA<T> { public: void DrivedUseAFunc() { UseA<T>::useAFunc(); } } |
但是第三种办法有明显的缺陷:显示指明调用基类函数会是得动态绑定不会发生。
其实,它们做的事情都是相同的:对编译器承诺基类模板的任何特化版本都将支持其泛化版本所提供的接口。
其实,它们做的事情都是相同的:对编译器承诺基类模板的任何特化版本都将支持其泛化版本所提供的接口。
总之可以在派生类中通过this指针或者using声明来告诉派生类使用的是基类的函数。
条款44:将与参数无关的代码抽离templates
1.模板类生成多个类和多个函数,所以
任何模板类代码都不该与某个造成膨胀的模板参数产生相依关系。例如一个正方矩阵具有一个矩阵求逆的成员函数invert():
C++ Code
1
2 3 4 5 6 7 |
template<
typename T, std::size_t n>
//该模板类支持n*n矩阵,元素类型为T,std::size_t属于非类型参数 class SquareMatrix { public: void invert(); //矩阵求逆运算 ... }; |
如果这个模板类在具现化时接受相同元素类型(如double)而大小不同的参数时,便可能生成两份invert()函数,造成代码膨胀。代码如下:
C++ Code
1
2 3 4 5 6 |
SquareMatrix<
double,
5> sm1;
... sm1.invert(); //调用SquareMatrix<double,5>::invert SquareMatrix< double, 10> sm2; ... sm2.invert(); //调用SquareMatrix<double,10>::invert |
PS:正如作者后面所说,代码膨胀的版本有可能在性能上更优,因此实际选择上可能无法避免。书中很多建议都是这样——照着做了会有好处,但是也会丧失一些其他优点——这需要在实际应用中权衡选择。
2.对于非类型参数(如上面例子中的std::size_t)引起的代码膨胀,往往可以消除,做法是以函数参数或类成员变量替换模板参数。
例如上面的例子中,可以在基类中实现invert()函数,但只提供包含矩阵大小的参数,这样真正的矩阵类就可以以派生类的方式调用该函数,多个相同元素类型而不同尺寸的矩阵对象就可以调用一份带有参数的invert()函数:
例如上面的例子中,可以在基类中实现invert()函数,但只提供包含矩阵大小的参数,这样真正的矩阵类就可以以派生类的方式调用该函数,多个相同元素类型而不同尺寸的矩阵对象就可以调用一份带有参数的invert()函数:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
template<
typename T>
//与尺寸无关的基类 class SquareMatrixBase { protected: SquareMatrixBase(std::size_t n, T *pMem) : size(n), pData(pMem) {} void setDataPtr(T *ptr) { pData = ptr; //存储矩阵大小和一个指针指向矩阵内容 } void invert(std::size_t matrixSize); //以给定尺寸求逆 ... private: std::size_t size; //矩阵大小 T *pData; //指针,指向矩阵的内容,否则invert()不知道要操作什么类型的数据。有些实现版本也可以在类内部直接分配内存来初始化矩阵内容,也可以通过动态内存分配存储内容 }; template< typename T, std::size_t n> class SquareMatrix : private SquareMatrixBase<T> //只是调用基类中的实现,而非为了表现出“is-a“关系,故使用私有继承 { private: using SquareMatrixBase<T>::invert; //避免同名函数的名称遮掩 ... public: void invert() { this->invert(n); //内联函数,调用基类版本的invert(),同样this指针也是为了防止同名函数的遮掩 } ... }; |
3.因类型参数而造成的代码膨胀,往往可以降低,做法是让带有完全相同二进制表述的具现类型共享实现码。
比如许多平台上int和long有相同的二进制表述,连接器可以实现代码合并;而大多数平台上所有的指针类型都有相同的二进制表述,因此凡模板类或模板函数包含指针的,往往应该对每一个成员函数使用唯一一份底层实现(调用无类型指针完成相同的操作)。
比如许多平台上int和long有相同的二进制表述,连接器可以实现代码合并;而大多数平台上所有的指针类型都有相同的二进制表述,因此凡模板类或模板函数包含指针的,往往应该对每一个成员函数使用唯一一份底层实现(调用无类型指针完成相同的操作)。
条款45:运用成员函数模板接受所有兼容类型
不知道大家注意到没有,在类的继承体系中,对象指针一直在为我们做一件很好的事情:支持隐式转换(implicit conversions).Derived class指针可以隐式转换为base class指针,"指向non-const对象"的指针可以转换为"指向const对象"......等等.下面例子展示了继承体系之间的可能进行的转换操作:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Top
{ ... }; class Middle: public Top { ... }; class Bottom: public Middle { ... }; Top *top1 = new Middle; //将Middle*转换为Top* Top *top2 = new Bottom; //将Bottom*转换为Top* const Top *const_top2 = top1; //将Top*转换为const Top* |
在前面的条款13中我们提到,像std::auto_ptr和tr1::shared_ptr这样的智能指针,能够提供原始指针没有的机能.比如能够在正确的时机自动删除heap-based资源.本款中我们自然就想到了,如果智能指针也能支持上述的隐式操作转换,那岂不是很方便.于是我们在这里试图让类似下面的代码通过编译:
C++ Code
1
2 3 4 5 6 7 8 9 10 |
template <
typename T>
class SmartPtr { public: explicit SmartPtr(T *realPtr); //智能指针通常以原始指针完成初始化 ... }; SmartPtr<Top> top1_smart_ptr = SmartPtr<Middle>( new Middle); SmartPtr<Top> top2_smart_ptr = SmartPtr<Bottom>( new Bottom); SmartPtr< const Top> const_top2_ptr = top1_smart_ptr; |
但是,现实的情况好像并不是像我们想象的那么美好:同一个template的不同具现体(instantiation)之间并不存在故有的联系.也就是说,编译器视SmartPtr<Middle>和SmartPtr<Top>为完全不同的classes.它们之间没有任何必然的联系.这个问题不小啊,但一想到如果SmartPtr classes之间具有了这样的转换能力,世界将会变的更加的美好.我就增强了要实现这种模拟的信心.好,接着往下看.
在上述继承体系中,每一条语句都创建一个新式智能指针对象,我们自然就想到了,我们应该把工作的焦点放在如何编写智能指针的构造函数上,使其满足我们的转型需要.而稍微仔细的观察一会儿,我们就有了一个新的顾忌:我们永远无法写出我们需要的构造函数.为什么呢?因为继承基类的子类可以有很多个,每诞生一个新的子类,我们就必须要在基类智能指针中添加一个为实现其向子类转换的新的构造函数.那样的代码不仅理解起来很晦涩,更难以维护.在如此"绝境"之下我们想到了模板成员函数,有了这样的"利器"我们就在战术上从被动为主动了,哈哈.
解决方案:
C++ Code
1
2 3 4 5 6 7 8 |
template <
typename T>
class SmartPtr { public: template < typename U> SmartPtr( const SmartPtr<U> &other); //copy constructor ... }; |
等一下,构造函数为什么没有explicit修饰?我故意的.因为要完成原始指针之间的隐式转换,我们需要支持这样的操作.如果SmartPtr也像auto_ptr和tr1::shared_ptr一样,提供一个get成员函数去发挥智能指针对象的原始指针副本.上面的代码我们可以写的更清楚一点:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
template <
typename T>
class SmartPtr { public: template < typename U> SmartPtr( const SmartPtr<U> &other) : held_ptr_( other.get() ) { ... } //用other.held_ptr_初始化this->held_ptr_,这里的other的原始对象如果是 //this原始对象的子类的话,这里就完成子类向父类的隐式转换过程. T *get() const { return held_ptr_; } ... private: T *held_ptr_; //这是SmartPtr持有的内置指针. }; |
呵呵,不错吧!其实member function template(成员函数模板的效用不限于构造函数),它们常常扮演的另一个角色就是支持赋值操作.这点在TR1的shared_ptr中获得了绝好的发挥.下面是TR1规范中关于tr1::shared_ptr的一份摘录.
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
template<
class T>
class shared_ptr { public: template< class Y> explicit shared_ptr(Y *p); template< class Y> shared_ptr(shared_ptr<Y> const &r); template< class Y> explicit shared_ptr(weak_ptr<Y> const &r); template< class Y> explicit shared_ptr(auto_ptr<Y> &r); //为什么这里不要const? 因为当你复制一个auto_ptr,它们其实被改动了. template< class Y> shared_ptr & operator=(shared_ptr<Y> const &r); template< class Y> shared_ptr & operator=(auto_ptr<Y> &r); //为什么这里不要const? 原因同上 ... }; |
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 |
template<
class T>
class shared_ptr { public: shared_ptr(shared_ptr const &r); //copy 构造函数 template< class Y> shared_ptr(shared_ptr<Y> const &r); //泛化的copy构造 shared_ptr & operator=(shared_ptr const &r); //copy assignment template< class Y> shared_ptr & operator=(shared_ptr<Y> const &r); //泛化copy assignment ... }; |
■ 请使用member function template(成员函数模板)生成"可几首所有兼容类型"的函数
■ 如果你声明member template用于"泛化copy构造"或"泛化assignment操作",你还需要声明正常copy构造函数和copy assignment操作符.
条款46:需要类型转换时请为模板定义非成员函数
还记得在条款24中,我们提到只有non-member函数才有能力'在所有实参身上实施隐式类型转换'.今天我们讨论的
东西与该条款相关,如果现在的你恰好忘记了前面这条款的内容,那么我建议你还是先把那条款在消化一下,再开始
这一款吧,呵呵,'磨刀不误砍柴工'嘛!废话我就不多说了,我们进入正题.
东西与该条款相关,如果现在的你恰好忘记了前面这条款的内容,那么我建议你还是先把那条款在消化一下,再开始
这一款吧,呵呵,'磨刀不误砍柴工'嘛!废话我就不多说了,我们进入正题.
'只有non-member函数才有能力在所有实参身上实施隐式类型转换',在泛型编程的世界里,这是否也成立呢?下面我们来稍微修改一下前面这款例子中的代码,然后验证一下是这句话是否依然成立.
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 |
template<
typename T>
class Rational { public: Rational( const T &numberator = 0, const T &denominator = 1); const T numerator() const; const T denominator() const; ... }; template< typename T> const Rational<T> operator*( const Rational<T> &left_handle_side, const Rational<T> &right_handle_side ); |
C++ Code
1
2 |
Rational<
int> one_half(
1,
2 );
Rational< int> result = one_half * 2; //compile error. |
受两个Rationals参数的那个operator*啦), 但在这里编译器却不知道我们要调用哪个函数,它们试图想出什么函数被
名为operator*的template具现化出来.但现在的问题是它们没有足够大的能耐来完成如此的工作.为了完成具现化工作,必须先算出T是什么,于是它们开始了下面的尝试:
编译器首先看到了operator*的两个参数,它们的类型分别是Rational<int>(one_half类型)和int(2的类型),每个参数分开考虑.以one_half进行推导比较容易,operator*的第一个参数声明为Rational<T>,而传递给函数的第一实参类型是Rational<int>,故T一定int.但到了第二个参数推导的时候,问题就来了.operator*声明的第二个参数为Rational<T>,但实参确实int类型(2).编译器如何推算T?会不会发生像条款24出现的隐式参数转换呢?(编译器适用Rational<int>的non-explicit构造函数将2转换为Rational<int>,进而将T推导为int),但事实却是很惨酷的:它们没有那样做.因为在template实参推导过程中从不将隐式类型转换函数纳入考虑.这下麻烦了,那有什么办法能够解决这一问题的呢?别急,其实我们只需要利用一个事实就可以缓和编译器在template实参推导方面受到的挑战,这个事实就是:template class内的friend声明式可以指涉某个特定的non-member函数.class templates并不依赖template实参推导(后者只施行于function templates身上),所以编译器总是能够在class Rational<T>具现化时得知T.
因此,我们的问题的解决方案就出来了:令Rational<T> class声明适当的operator*为其friend函数.
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 |
template<
typename T>
class Rational { public: ... //同上 friend const Rational operator* ( const Rational &left_handle_side, const Rational &right_handle_side ); }; template < typename T> const Rational<T> operator*( const Rational<T> &left_handle_side, const Rational<T> &right_handle_side ) { ... } |
就被具现化出来了,而作为过程的一部分,friend函数operator*也就被自动声明出来了,此时后者为一个具现的函数
而不是函数模板罗,因此编译器可在调用它时使用隐式转换,于是混合式调用就基本成功了!基本?难道还有未完成的
工作?呵呵,当你编译的时候没问题,是吧?你试试链接呢?竟然无法连接!!SHIT!怎么个情况????
现在我们回头来思考这个问题.混合式代码通过了编译是因为编译器知道我们要调用哪个函数,但那个函数只被
声明与Rational内,并没有被定义出来.而我们意图令此class外部的operator* template提供定义式,但是行不通----
----如果我们自己声明了一个函数,就有责任定义那个函数.既然我们没有提供定义式,连接器当然找不到它!
声明与Rational内,并没有被定义出来.而我们意图令此class外部的operator* template提供定义式,但是行不通----
----如果我们自己声明了一个函数,就有责任定义那个函数.既然我们没有提供定义式,连接器当然找不到它!
最简单的可行方法就是将operator*函数本体合并至其声明式内:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 |
template<
typename T>
class Rational { public: ... //同上 friend const Rational operator*( const Rational &left_handle_side, const Rational &right_handle_side ) { return Rational( left_handle_side.numerator() * right_handle_side.numerator(), left_handle_side.denominator() * right_handle_side.denominator() ); } }; |
简单的Build一下,我们就可以看到,对operator*的混合调用现在可以编译连接并执行了.哦也!operator*定义体放
到了类的内部成了inline函数,而inline声明会给类带来冲击.为了最小化这种冲击,我们可以令operator*不做任何事
情,只调用一个定义于class外部的辅助函数,当然,对本条款中的例子而言,这样做没有太大的意义,因为operator*只
是一个单行函数,但对于更复杂的函数而言,这样做也许就有价值.本款的例子典型长成这样:(终结方案)
到了类的内部成了inline函数,而inline声明会给类带来冲击.为了最小化这种冲击,我们可以令operator*不做任何事
情,只调用一个定义于class外部的辅助函数,当然,对本条款中的例子而言,这样做没有太大的意义,因为operator*只
是一个单行函数,但对于更复杂的函数而言,这样做也许就有价值.本款的例子典型长成这样:(终结方案)
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
template<
typename T>
class Rational;
template< typename T> const Rational<T> doMultiply( const Rational<T> &left_handle_side, const Rational<T> &right_handle_side); template< typename T> class Rational { public: ... friend const Rational<T> operator*( const Rational<T> &left_handle_side, const Rational<T> &right_handle_side) { return doMultiply( left_handle_side, right_handle_side ); } ... }; |
请记住:
■ 当我们编写一个class template,而它所提供之'于此template相关的'函数支持'所有参数之隐式类型转换'时,请将
那些函数为'class template内部的friend函数'.
条款47:请使用traits classes表现类型信息
今天我们讨论的这款内容涉及到一个STL实现上的一个关键技术traits技术,简单的说就是类的型别判定技术.由于本条款要讨论内容比较多,所以我将分成两篇文章来表述,今天我们只讨论traits classes的实现,明天我将讨论其使用,废话我就不多说了,我们现在就开始:
我们知道STL迭代器可分为五类,我再来简单的唠叨一下:
input迭代器只能向前移动,一次一步,客户只可读取(不能修改)它们所指的内容,而且只能读取一次;
output迭代器情况似,只是为了输出;以上这两种分别模仿文件的读写指针,分类的代表为istream_iterators和ostream_iterators.它们两个属于能力最小的迭代器分类,只适合一次性操作算法.
forward迭代器,该种迭代器能够做上述两种类所能做的每一件事情,而且可以读写所指物一次以上.
bidirectional迭代器,比前一种威力更大,除了可以向前移动还可以向后动.STLlist,set,multiset,map,multimap的迭代器都是属于这一分类.最后一种分类也是威力最强的迭代器当属random access迭代器,它可以在常量时间内向前或向后迢遥任意距离.vector,deque和string提供的迭代器就属于这一分类.
对于这五种分类,C++标准库提供专门的类型标记结构对它们进行区分:
C++ Code
1
2 3 4 5 |
struct input_iterator_tag {};
struct output_iterator_tag {}; struct forward_iterator_tag: public input_iterator_tag {}; struct bidirectional_iterator_tag: public forward_iterator_tag {}; struct random_access_iterator_tag: public bidirectional_iterator_tag {}; |
下面我们来看STL算法里面的函数advance的实现,其作用就是将某个迭代器移动某个距离:
C++ Code
1
2 |
template <
typename IterT,
typename DistT>
void advance(IterT &iter, DistT d); |
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
template <
typename IterT,
typename DistT>
void advance(IterT &iter, DistT d) { if( iter is a random access iterator ) { iter += d; //针对random access迭代器使用迭代器算术运算 } else //针对其它迭代器分类反复调用++或者-- { if( d >= 0 ) { while( d-- ) ++iter; } else { while( d++ ) --iter; } } } |
当写完上面的伪以后,我们就会立刻将焦点转移到一个点上:必须能够判断iter是否为random access迭代器,也就是说需要知道类型IterT是否为random access迭代器分类.换句话说我们需要取得类型的某些信息.那就是traits让你得以进行的事:它们允许你在编译期间取得某些类型信息.
Traits并不是C++关键字或一个预先定义好的构件;它们是一种技术,该技术的要求之一是,它对内置类型和用户自定义类型的表现必须一样好.这就意味着'类型内的嵌套信息'这种东西出局了,因为我们无法将信息嵌套于原始指针内.因此类型的traits信息必须位于类型自身之外.标识程序库的做法是把它放进一个template及其一个或多个特化版本中.这样的templates在标准程序库中有若干个,其中针对迭代器者被命名为iterator_traits:
C++ Code
1
2 |
template<
typename IterT>
struct iterator_traits; |
iterator_traits以两个部分实现上述所言.首先它约定每一个用户自定义迭代器类型必须嵌套一个typedef,名为iterator_category,用来确认类型信息.比如deque可随机访问,所以针对deque迭代器而设计的class看起来会是这个样子:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 |
template<...>
class deque { public: class iterator { public: typedef random_access_iterator_tag iterator_category; ... }; ... }; |
list迭代器可双向行进,所以应该这样:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 |
template<...>
class list { public: class iterator { public: typedef bidirectional_iterator_tag iterator_category; ... }; ... }; |
对于iterator_traits只要响应iterator class的嵌套式typedef即可:
C++ Code
1
2 3 4 5 6 |
template <
typename IterT>
struct iterator_traits { typedef typename IterT::iterator_category iterator_category; ... }; |
上面的对用户自定义类型行得通,但是仅仅这样的iterator_traits是不够的,你必须还要提供对指针(也是一种迭代器)的支持,因为指针不可能嵌套typedef.于是我们就让iterator_traits的第二部分专门对付指针,它所利用的工具就是模板的偏特化技术.由于指针的行径与random access迭代器类似,所以iterator_traits为指针指定的迭代器类型如下:
C++ Code
1
2 3 4 5 6 |
template<
typename IterT>
struct iterator_traits<IterT *> { typedef random_access_iterator_tag iterator_category; ... }; |
■ 确认若干你希望将来可取得的类型信息.
■ 为该信息选一个名称(例如iterator_category)
■ 提供一个template和一组特化版本,内含你希望支持的类型相关信息.
好了,今天就到此为止,明天我们继续讨论.
C++ Code
1
2 3 4 5 6 7 |
template<
typename IterT,
typename DistT>
void advance(IterT &iter, DistT d) { if( typeid( typename std::iterator_traits<IterT>::iterator_category ) == typeid ( std::random_access_iterator_tag ) ) ... } |
这看起来还行是吧,其实它却有些潜在的问题.首先将导致编译问题,我将在下一款讨论这一点,我们现在应该关心更根
本的问题.IterT类型在编译期间获知,所以iterator_traits<IterT>::iterator_category也可在编译期间确定.但if语句确是在运行期才会核定.为什么将可在编译期完成的事情延期到运行期才做呢?这不仅浪费时间,也造成可执行文件膨胀.我们真正想要的是一个条件式来判断'编译器核定成功'的类型.恰巧C++的重载机制就满足这个需求,哇哈哈!
本的问题.IterT类型在编译期间获知,所以iterator_traits<IterT>::iterator_category也可在编译期间确定.但if语句确是在运行期才会核定.为什么将可在编译期完成的事情延期到运行期才做呢?这不仅浪费时间,也造成可执行文件膨胀.我们真正想要的是一个条件式来判断'编译器核定成功'的类型.恰巧C++的重载机制就满足这个需求,哇哈哈!
由于重载函数是根据传来的实参选择最佳的重载体,所以为了让advance的行为如我们所期望,我们需要产生几个接受不同类型的iterator_category对象作参数的函数,我们将这函数取名为doAdvance:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
//这份实现用于random access迭代器 template< typename IterT, typename DistT> void doAdvance( IterT &iter, DistT d, std::random_access_iterator_tag) { iter += d; } //这份实现用于bidirectional迭代器 template< typename IterT, typename DistT> void doAdvance( IterT &iter, DistT d, std::bidirectional_iterator_tag) { if( d >= 0 ) { while( d-- ) { ++iter; } } else { while( d++ ) { --iter; } } } //这份实现用于input迭代器 template< typename IterT, typename DistT> void doAdvance( IterT &iter, DistT d, std::input_iterator_tag) { if( d < 0 ) { throw std::out_of_range( "Negative distance" ); } while( d-- ) { ++iter; } } |
C++ Code
1
2 3 4 5 |
template<
typename IterT,
typename DistT>
void advance( IterT &iter, DistT &d ) { doAdvance( iter, d, typename std::iterator_traits<IterT>::iterator_category() ); } |
啦啦啦啦,搞定!现在我们来总结一下如何使用一个traits class了:
■ 建立一组重载函数或模板函数,彼此之间差异只在于各自的traits参数.令每个函数实现码与其接受之traits
信息相应和.
■ 建立一个控制函数或函数模板,它调用上述重载函数并传递traits class所提供的信息.
好了,今天任务完成!
请记住:
■ Traits classes使得'类型相关信息'在编译期可用.它们以templates和'templates特化'完成实现.
■ 整合重载技术后,traits classes有可能在编译期对类型执行if...else测试.
■ 建立一组重载函数或模板函数,彼此之间差异只在于各自的traits参数.令每个函数实现码与其接受之traits
信息相应和.
■ 建立一个控制函数或函数模板,它调用上述重载函数并传递traits class所提供的信息.
好了,今天任务完成!
请记住:
■ Traits classes使得'类型相关信息'在编译期可用.它们以templates和'templates特化'完成实现.
■ 整合重载技术后,traits classes有可能在编译期对类型执行if...else测试.
条款48:认识template 元编程
Template metaprogramming(TMPS,模板元编程)是编写template-based C++程序并执行编译期的过程.它本质上是以C++写成、执行于C++编译器内的程序.一旦TMP程序结束执行,其输出,也就是从templates具现出来的若干C++源码,便会一如往常地被编译.TMP有两个伟大的效力.第一,它让某些事情更容易.如果没有它,那些事情将是困难,甚至不可能.第二,欲与TMP执行于C++编译期,因此可将工作从运行期转移到编译期.这导致一个结果是,某些错误原本通常在运行期才能检测到,现在可在编译期找出来.这样带来的结果是很美好的.因为使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存要求.然而它也会带来另一个令人感到不愉快的结果是,编译时间变长了.
前面一款我们提到了使用typeid的advance的伪代码实现版本:
前面一款我们提到了使用typeid的advance的伪代码实现版本:
C++ Code
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
template <
typename IterT,
typename DistT>
void advance( IterT &iter, DistT d ) { if( typeid( typename std::iterator_traits<IterT>::iterator_category ) == typeid( std::random_access_iterator_tag ) ) { iter += d; } else { if( d >= 0 ) { while( d-- ) { ++iter; } } else { while( d++ ) { --iter; } } } } |
上一条款中我曾提过advance的typeid-based实现方式可能导致编译期问题,我们现在就写出如下代码测试:
C++ Code
1
2 3 4 5 |
std::list<
int>::iterator iter;
... advance( iter, 10 ); //compile error:error C2676: binary '+=' : 'std::list<_Ty>:: //_Iterator<_Secure_validation>' does not define this operator or a conversion to //a type acceptable to the predefined operator. |
上述错误信息显示的是iter += d;由于这行代码导致的.这怎么可能?list<int>::iterator是bidirectional迭代器,并不支持+=,只有random access迭代器才支持,此刻我们知道编译器绝对不会执行+=那一行代码的,因为测试typeid那行的if句总是不成立.你开始郁闷了吧?我在这里要说的是:在用TMP编写的C++程序中,编译器必须确保所有源码都有效,纵使是不会执行的代码.晕,原来还有这样的啊,其实你仔细想一想,这样做也是可以理解的:TMP程序被具现化以后发生在编译期,编译器必须依靠执行的具现化代码才能确定哪些语句能执行到,为了对所有可能的具现化代码编译的支持,编译器也只能这样做!葛大爷在他的一部影片中说了一句:'有枣没枣打一杆,宁可错杀一千,不愿放走一个.',要是用在这里的话也有一定的道理,呵呵.
为求领悟TMP之所以值得学习,很重要一点是先对它能够表达的目标有一个比较好的理解,原书上对下面的每一个点都举了例子进行阐述,我对这些阐述还没有理解的足够的深刻,所以在这里我也不敢误人子弟地阐述自己的理解.我只帖出来原书上出现的三个TMP运用的例子,具体的阐述过程,我建议各位还是看原书上的阐述吧!这三个例子为:
■ 确保度量单位正确.
■ 优化矩阵运算.
■ 可以生成客户定制之设计模式实现品.
TMP或许永远不会成为主流,但对某些程序员--特别是程序库开发人员---几乎确定成为他们的主要粮食.
请记住:
■ TMP可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率.
■ TMP可别用来生成'基于政策选择组合'的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码.
为求领悟TMP之所以值得学习,很重要一点是先对它能够表达的目标有一个比较好的理解,原书上对下面的每一个点都举了例子进行阐述,我对这些阐述还没有理解的足够的深刻,所以在这里我也不敢误人子弟地阐述自己的理解.我只帖出来原书上出现的三个TMP运用的例子,具体的阐述过程,我建议各位还是看原书上的阐述吧!这三个例子为:
■ 确保度量单位正确.
■ 优化矩阵运算.
■ 可以生成客户定制之设计模式实现品.
TMP或许永远不会成为主流,但对某些程序员--特别是程序库开发人员---几乎确定成为他们的主要粮食.
请记住:
■ TMP可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率.
■ TMP可别用来生成'基于政策选择组合'的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码.