从此开始进入模板与泛型编程
,这也是一般C++
程序员较少接触的部分。如果读者有使用C++
里的标准模板库(Standard Template Librarry,STL)
的话,能够多多少少地体会到模板
的威力。
正如书上所说,这里的内容不能让读者成为专家级的template
程序员,但可以让读者成为一个比较好的template
程序员。笔者认为这里的意思是介绍一些模板编程的基本知识,例如更好地使用STL,或者进行一些简单的模板编程。因为一些更加进阶的玩法,例如在编译器利用模板编程完成某些运算等这些内容,要对模板元编程(Template Metaprogramming)
十分熟悉,这要求编程人员对C++
的语法特性十分了解,甚至要对准备编译源文件的编译器十分了解。
笔者自上个主题后期开始的条款就呈现出经验不足,对条款的解释能力开始下降,总结一下无非自己的编程经历也只能理解到书的前半部分,后面部分笔者仍然任重而道远。笔者在剩下的部分里尽自己最大的能力,不足的地方请多指教。
条款41:了解隐式接口和编译期多态 有关C++模板
编程的原理,读者们可以读《C++ Template》这本书了解。在开始介绍条款之前,读者们首先要了解模板是怎么编译的,例如:
1 2 3 4 5 6 7 8 template <typename T>class List { public : T operator [](int idx) const ; ... };
在编程中,这个类模板
被使用了:
在编译期,template是不会被编译的,而是先产生一个模板类
:
1 2 3 4 5 6 7 class List { public : int operator [](int idx) const ; ... };
然后编译器才会去编译这个类模板
的实例。介绍这个原理的意义在于要说明,在类模板
有实例之前,类模板里面有什么内容,编译器是不会理会的,例如:
1 2 3 4 5 6 7 8 9 template <typename T>class Container { public : void fun (const T& obj) { obj.call (); } };
如果来了一个Container<float>
,可想而知,当然是报错的。编译器也不会对类模板
以及模板类
有太多的检查,只要产生出来的模板类有符合模板类里面有的行为即可。笔者仿照书上的例子来说明结合隐式类型转换
,模板编程会变得多困难:
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 39 40 41 class A { public : operator int () () { return 33 ; } }; class B { public : A& size () { return a; } private : A a; }; template <typename T>void fun (T& obj) { if (obj.size () > 10 ) { cout << "Greater than 10." << endl; } else { cout << "Less than 10" << endl; } } int main (int argc, char * argv) { B b; fun (b); return 0 ; }
看到输出:
也就是说,fun
里面,B的实例
是符合有size
函数这个约束的,而B::size()
返回的A类型
不能与int
直接比较,而是调用了A::operator int()
作了隐式转换之后比较。
这个例子要说明的是,模板推断
的双面性,一方面类型推断
的能力为编程人员带来了遍历,可以少写一些类型转换等语句,也就是语法糖吧;坏处就是推断太强大了,以至于做了隐式转换
可能产生非预期的结果。
而这个推断
是基于有效表达式
的。这也是书上要介绍的编译期多态
,也就是在编译器决议出用什么版本的函数。
条款42:了解typename的双重意义 在声明模板的时候:
1 2 3 template <typename T>class Container {...};
与:
1 2 3 template <class T >class Container {...};
里的typename
和class
是等价的关键字,但是在用到某个类模板
里的内置类型时,例如:
1 std::vector<Object>::iterator iter;
然而有可能在编译过程中,编译器可能认为std::vector<Object>::iterator
是vector
里面的一个成员变量,而真实情况是vector
里面的一个嵌套从属类型
,编译器有可能会报错。这时改成:
1 typename std::vector<Object>::iterator iter;
问题解决,这时候的typename
是不能用class
关键字代替的,这里的typename
声明std::vector<Object>::iterator
是一个类型,而不是某种实例变量。
条款43:学习处理模板化基类内的名称 该条款涉及到:
笔者反复看这个条款,最后也完全理解这个条款想讲的是什么。但是推断应该是与类设计
相关的问题。引用书上的例子:
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 39 40 41 42 43 class CompanyA { public : void sendCleartext (const std::string& msg) ; void sendEncryted (const std::string& msg) ; ... }; class CompanyB { public : void sendCleartext (const std::string& msg) ; void sendEncryted (const std::string& msg) ; ... }; ... class MsgInfo {...}; template <typename Company>class MsgSender { public : ... void sendClear (const MsgInfo& info) { std::string msg; Company c; c.sendCleartext (msg); } void sendSecret (const MsgInfo& info) { std::string msg; Company c; c.sendEncryted (msg); } };
接着为了让发送消息有日志,编写一个可以记日志的LoggingMsgSender
:
1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename Company>class LoggingMsgSender : public MsgSender<Company>{ public : ... void sendClearMsg (const MsgInfo& info) { ... sendClear (info); ... } ... };
问题的根源在于,当编译器处理class template LoggingMsgSender
定义式时,首先要知道MsgSender
的定义式,但是MsgSender
是一个模板,也就是说需要一个MsgSender<Company>
的实例的定义式。而MsgSender
有可能有特化
的版本,特化
的版本与一般的模板又不一样,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class CompanyZ { public : ... void sendEncryted (const std::string& msg) ; ... }; template <> class MsgSender <CompanyZ>{ public : ... void sendSecret (const MsgInfo& info) {...}};
这个时候,LoggingMsgSender
:
1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename Company>class LoggingMsgSender : public MsgSender<Company>{ public : ... void sendClearMsg (const MsgInfo& info) { ... sendClear (info); ... } ... };
因为MsgSender
有可能存在全特化的版本,因而编译器不会去处理基类相关的内容。
为了能够正常编译代码,有三个办法(假设所有的Company行为一致,不会像CompanyZ有与众不同的行为):
用this
关键字1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename Company>class LoggingMsgSender : public MsgSender<Company>{ public : ... void sendClearMsg (const MsgInfo& info) { ... this ->sendClear (info); ... } ... };
用using
声明式:1 2 3 4 5 6 7 8 9 10 11 12 13 14 template <typename Company>class LoggingMsgSender : public MsgSender<Company>{ public : using MsgSender<Company>::sendClear; ... void sendClearMsg (const MsgInfo& info) { ... sendClear (info); ... } ... };
显式指出函数的版本:1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename Company>class LoggingMsgSender : public MsgSender<Company>{ public : ... void sendClearMsg (const MsgInfo& info) { ... MsgSender<Company>::sendClear (info); ... } ... };
第二三个方法显式地声明了函数,会使得virtual
函数的多态行为失效。
还有,如果采用了解决方案之后,还出现LoggingMsgSender<CompanyZ>
的实例化,编译当然就失败了。
条款44:将与参数无关的代码抽离templates 该条款第一个提到的问题是非参数类型
带来的代码膨胀
,template
支持如下的语法:
1 2 3 template <typename T, std::size_t size> class SquareMatrix {...};
在写下:
1 2 3 4 5 SquareMatrix<int , 3 > m1; SquareMatrix<int , 4 > m2; ...
编译器会为SquareMatrix<int, 3>
以及SquareMatrix<int, 4>
模板类各自生成实例,可能产生如下的代码:
1 2 3 4 5 6 7 8 9 10 11 class I3SquareMatrix { public : ... }; class I4SquareMatrix { public : ... };
当需要产生很多大小不一的方阵实例的时候,类模板
的实例代码随之膨胀。然而仅仅只是因为尺寸的问题就带来代码膨胀,这是不必要的。通过成员变量
一般可以解决问题:
1 2 3 4 5 6 7 8 9 10 11 template <typename T>class SquareMatrix { public : ... private : ... std::size_t matrix_size; T* data_ptr; ... };
这样就解决了来自于非参数类型
导致的代码膨胀。
而对于来自与类型参数
的代码膨胀,一般不需要处理,要么就要有高超的编程技巧。例如SquareMatrix<int>
、SquareMatrix<const int>
和SquareMatrix<long>
明明在二进制表示
上一致(一般平台int和long一样),但是编译器会分别为它们生成不同的类模板实例。往往这样做的话需要用到void*
类型来操作一些手法,这个就是很底层的代码的问题了,而且STL
大部分容器是做了这个优化的,因而笔者认为该部分大多数人了解即可。
条款45:运用成员函数模板接受所有兼容类型 书上用了智能指针
来作说明,如下一个继承体系:
1 2 3 4 5 class Top {...};class Middle : public Top {...};class Bottom : public Middle {...};
第一版的智能指针
类定义:
1 2 3 4 5 6 7 template <typename T>class SmartPtr { public : explicit SmartPtr (T* ptr) ; ... };
原生的继承体系,用父类的指针指向子类实例:
1 2 3 Top* top1 = new Middle; Top* top2 = new Bottom;
都没有任何问题,但是:
1 2 3 4 5 6 7 8 9 10 void fun (SmartPtr<Top> top) { } ... SmartPtr<Middle> middlePtr (new Middle) ; fun (middlePtr);
但是问题就出现在这里了,原生的继承体系Top<-Middle<-Bottom
是没有问题的,一般的编译器会为上面的使用生成如下的类模板实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class TopSmartPtr { public : explicit TopSmartPtr (Top* ptr) ; ... }; class MiddleSmartPtr { public : explicit MiddleSmartPtr (Middle* ptr) ; ... };
注意并不是:
1 2 class MiddleSmartPtr : public TopSmartPtr{...};
也就是说SmartPtr<Middle>
与SmartPtr<Top>
没有任何关系,目前也没有代码提供它们之间的显式
或者隐式
类型转换,自然编译不能通过了。
为了解决这个问题,让SmartPtr<Top> top(SmartPtr<Middle>(new Middle))
这类的语法能够正常工作,SmartPtr
的类定义则应该这么写:
1 2 3 4 5 6 7 8 9 10 11 template <typname T>class SmartPtr { public : template <typename U> explicit SmartPtr (U* instance) ; template <typename U> SmartPtr& operator =(const SmartPtr<U>& sptr); ... };
更具体的设计可以查看std::shared_ptr
的源码,在这里要提示一点,在写下上面的成员函数的定义时,应该是:
1 2 3 4 5 6 template <typename T> template <typename U> SmartPtr<T>& operator =(const SmartPtr<U>& sptr) { ... }
而不能写成不能通过的:
1 2 3 4 5 template <typename T, typename U>SmartPtr<T>& operator =(const SmartPtr<U>& sptr) { ... }
这样就解决了泛化的问题。
条款46:需要类型转换时请为模板定义非成员函数 问题类似与条款24,不同的是这次加入了模板,假设有个有理数的类模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename T>class Rational { public : Rational (const T& numerator = 0 , const T& denominator); const T numerator () const ; const T denominator () const ; ... }; template <typename T>const Rational<T> operator *(const Rational<T>& lhs, const Rational<T>& rhs){...}
根据常理,有理数直接的操作:
1 2 3 Rational<int > oneHalf (1 , 2 ) ;Rational result = oneHalf * 2 ;
应该是没问题的,但是在强类型
语言中,编译器试图寻找匹配的函数,但是目前也只能匹配到:
1 2 template <typename T>const Rational<T> operator *(const Rational<T>& lhs, const Rational<T>& rhs)
显然根据地一个参数,Rational<int>
被推导出来了,然而template实参推导过程
不会考虑通过构造函数而发生
的隐式类型转换。在编程人员尚未提供规则使得int
可以向const Rational<int>
转换,在产生模板函数过程自然推导失败了。
书上提供的解决方法:
1 2 3 4 5 6 7 8 9 10 11 12 template <typename T>class Rational { public : ... friend const Rational operator *(const Rational& lhs, const Rational& rhs); }; template <typename T>const Rational<T> operator *(const Rational& lhs, const Rational& rhs){...}
这样做的原理是让编译器在产生Rational<int>
类模板实例时,带上友元函数一起产生,使其省略上一个版本让编译器先为operator*
推导的步骤,然后迫使oneHalf * 2
套用函数时,对int
强制做隐式类型转换。
编译通过,但是会链接失败,神奇的是,把operator*
的定义放回类内部又可以了:
1 2 3 4 5 6 7 8 9 10 template <typename T>class Rational { public : ... friend const Rational operator *(const Rational& lhs, const Rational& rhs) { ... } };
friend
关键字原本是用于让类外部的函数,或者类,可以访问私有成员,但是这里并没有这样的使用。
当类模板相关的函数需要隐式类型转换的时候,将函数定义为类模板内部的friend函数
。
条款47:请使用traits classes表现类型信息 这个条款对于写轮子的人来说比较有用,STL
用的人应该不少,但是阅读过STL源码的人可能不多,连笔者至今也没有系统读过C++ STL的源码,而STL中才会有大量涉及traits classes
代码。
Traits并不是C++关键字或者一个预先定义好的构建:它们是一种技术,也是一个C++程序员共同遵守的协议。
因为模板编程的初衷是将与类型无关的操作提取出来,减少代码的重复性。不从实现角度来讨论,在该条款里面,模板编程表现得像接口
一样,引用书上advance
函数的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename IterT, typename DistT>void advance (IterT& iter, DistT d) { if (iter是一个随机访问的迭代器) { iter += d; } else { if (d >= 0 ){while (d--) ++iter;} else {while (d++) --iter;} } }
在advance
例子里面,if
分支里面的描述用到的就是输入的迭代器的Traits
信息,假设迭代器的遵循Traits
协议:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename IterT>struct IteratorTraits { const static bool is_random_access = false ; ... }; ... template <> struct IteratorTraits <RandomAccessIterator> { const static bool is_random_access = true ; ... };
有了类似这样的实现,那么上面的advance
例子的判断语句可以具体为:
1 if (IterT::is_random_access)
这时的advance
就像是一个接口
,你只需要输入兼容的迭代器即可,无需关心该迭代器的特性会影响如何完成这个操作。
笔者前面的模拟实现是需要运行时的判断,而模板元编程强大之处,也是设计思想,尽可能地在编译期完成计算,提高运行时的效率。那么Traits
应该改成较为合理的形式(参考STL):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 template <...> class deque { public : class iterator { typedef random_access_iterator_tag iterator_category; ... }; ... }; template <typename IterT>struct iterator_traits { typedef typename IterT::iterator_category iterator_category; ... };
可以根据STL的设计,提供一个运行时的判断:
1 2 3 4 5 6 7 8 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)) ... }
为了优化运行时,同时不改变接口的情况下,这时候advance
更像一个接口了:
1 2 3 4 5 6 7 8 template <typename IterT, typename DistT>void doAdvance (IterT iter, DistT d, std::random_access_iterator_tag) { iter += d; } ...
这时的advance
仅仅是单纯转发(forwarding)
:
1 2 3 4 5 6 7 8 template <typename IterT, typename DistT>void advance (IterT iter, DistT d,) { doAdvance (iter, d, typename std::iterator_traits<IterT>::iterator_category) ); }
以上介绍了Traits Classes
的基本原理和实现。
条款48:认识template元编程 模板元编程(Template Metaprogramming,TMP)
是在编译期
将运算完成,以此减少在运行时的运算,但是滥用模板
也会导致代码膨胀
。更多的详细介绍
在给出例子之前首先说明模板元编程
里面的循环是用递归
进行模拟的,读者也可以回想到模板里面没有直接支持模板循环的语法。
来一个阶乘计算的例子:
1 2 3 4 5 6 7 8 9 10 11 template <unsigned n>struct Factorial { enum { value = n * Factorial<n-1 >::value }; }; template <>struct Factorial <0 > { enum { value = 1 }; };
然后写下:
1 2 cout << Factorial<5 >::value << endl; cout << Factorial<10 >::value << endl;
即可得到结果,当然TMP可以做更多的事情,但是也很考验程序员的设计能力。要是这么简单的话,TMP早就因其强大的编译期优化而流行了。
因此该条款也只是简单地介绍TMP,有兴趣的读者可以在网上搜索到更多的TMP奇淫技巧。