《Effective C++》:条款46-条款47
来源:互联网 发布:雷锋站长源码 编辑:程序博客网 时间:2024/05/02 07:17
- 条款46需要类型转换时请为模板定义非成员函数
- 条款47请使用traits class表现类型信息
条款46:需要类型转换时请为模板定义非成员函数
条款 24提到过为什么non-member函数才有能力“在所有实参身上实施隐式类型转换”,本条款接着那个Rational例子来讲,把Rational class模板化
template<typename T> class Rational{ public: Rational(const T& numerator=0,const T& denominator=1); const T numerator() const; const T denominator() const; …… }; template<typename T> const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs) {……}; Rational<int> oneHalf(1,2); Rational<int> result=oneHalf*2;//错误,无法通过编译
非模板的例子可以通过编译,但是模板化的例子就不行。在*条款**24,编译器直到我们尝试调用什么函数(就是接受两个Rational参数那个operator ),但是这里编译器不知道。编译器试图想什么函数被命名为operato* 的template具体化出来,它们知道自己可以具体化某个operator* 并接受两个Rational参数的函数,但为完成这一具体化行动,必须先算出T是什么。问题是它们没这个能耐。
看一下这个例子,编译器怎么推导T。本例中类型参数分别是Rational和int。operator* 的第一个参数被声明为Rational,传递给operator* 的第一实参(oneHalf)正类型正是Rational,所以T一定是int。operator* 的第二个参数类型被声明为Rational,但传递给 operator* 的第二个实参类型是int,编译器如何推算出T?或许你期望编译器使用Rational的non-explicit构造函数将2转换为Rational,进而推导出T为int,但它不这么做,因为在template实参推导过程中从不将隐式类型转换考虑在内。隐式转换在函数调用过程中的确被使用,但是在能够调用一个函数之前,首先要知道那个函数的存在;为了知道存在,必须先为相关的function template推导出参数类型(然后才可以将适当的函数具体化出来)。但是在template实参推导过程中不考虑通过构造函数发生的隐式类型转换。
现在解决编译器在template实参推导方面遇到的挑战,可以使用template class内的friend函数,因为template class内的friend声明式可以指涉某个特定的函数。也就是说class Rational可以说明operator* 是它的friend函数。class templates并不依赖template实参推导(后者只施行于function templates身上),所以编译器总是能够在class Rational具体化时得知T。所以令Rational class声明适当的operator*为friend函数,可以简化整个问题。
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<T>& lhs,const Rational<T>& rhs)//定义 {……};
这时候对operator* 的混合调用可以通过编译了。oneHalf被声明为一个Rational,class Rational被具体化出来,而作为过程的一部分,friend函数operator* (接受Rational参数)也就自动声明出来。后者身为一个函数而非函数模板,因此编译器在调用它的时候使用隐式转换(将int转换为Rational),所以混合调用可以通过编译。虽然通过编译,但是还会有链接问题,这个稍后再谈。先来看一下Rational内声明operator *的语法。
在一个class template内,template名称可被用来作为template和其参数的简略表达方式,所以在Rational内,我们可以把Rational简写为Rational。如果像下面这样写,一样有效
template<typename T> class Rational{ public: …… friend const Rational operator*(const Rational<T>& lhs,const Rational<T>& rhs);//声明 };
现在回头看一下刚刚说的链接的问题。虽然编译器直到我们调用的函数是接受Rational的那个operator * ,但是这个函数只有声明,没有定义。我们本来想让此class外部的operator * 提供定义式,但是这样行不通。如果我们自己声明了一个函数(Rational template内的作为),就有责任定义那个函数。如果没有定义,链接器就找不到它。一个最简单的办法就是将operator * 的定义合并到其声明内:
template<typename T> class Rational{ public: …… friend const Rational operator*(const Rational& lhs,const Rational& rhs);//声明+定义 { return Rational(lhs.numerator()*rhs.numerator(),lhs.denominator()*rhs.denominator()); } };
这个技术虽然使用了friend,却与传统的friend用途“访问class的non-public成员”不同。为了让类型转换可能发生与所有实参身上,我们需要一个non-member函数(**条款**24);为了让这个函数被自动具体化,我们需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是让它成为一个friend。
定义在class内部的函数都是inline函数,包括像operator * 这样的friend函数。为了将inline声明带来的冲击最小化,可以让operator * 调用定义在class外部的辅助函数。
template<typename T> class Rational;//forward decelarion template<typename T> const Rational<T> doMultiply(const Rational<T>& lhs,const Rational<T>& rhs); template<typename T> class Rational{ public: …… friend const Rational operator*(const Rational& lhs,const Rational& rhs);//声明+定义 { return doMultiply(lhs,rhs); } };
许多编译器会强迫你把template定义式放到头文件,所以有时你需要在头文件定义doMultiply
template<typename T> const Rational<T> doMultiply(const Rational<T>& lhs,const Rational<T>& rhs) { return Rational<T>(lhs.numerator()*rhs.numerator(),lhs.denominator()*rhs.denominator()); }
doMultiply是个template,自然不支持混合乘法,其实也没必要支持。它只是被operator * 调用,operator * 支持了混合乘法。
总结
- 当编写一个class template时,它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为class template内部的friend函数。
条款47:请使用traits class表现类型信息
STL主要由容器、迭代器和算法的templates构成,也包括若干工具性templates,其中有一个advance用来将迭代器移动某个给定距离:
template<typename IterT, typename DistT> void advance(IterT& iter, DistT d);//d大于零,向前移动,小于零则向后移动
表面上看,只是iterate+=d的动作,但是迭代器有5中,只有random access(随机访问)迭代器才支持+=操作。其他类型没这么大威力,只有反复++和–才行。
STL源码中关于迭代器的部分可以参考这里。这里也回顾一下这5中迭代器。
- input迭代器,它是read only,只能读取它指向的对象,且只能读取一次。它只能向前移动,一次一步。它模仿指向输入文件的阅读指针(read pointer);C++程序中的istream_iterators就是这类的代表。
- output迭代器,和input迭代器相反,它是write only。它也是只能向前移动,一次一步,且只能涂写一次它指向的对象,它模仿指向输出文件的涂写指针(write pointer);ostream_iterators是这一类代表。
- forward迭代器。这个迭代器派生自input迭代器,所以有input迭代器的所有功能。并且他可以读写指向的对象一次以上。
- bidirectional迭代器继承自forward迭代器,它的功能还包含向后移动。STL中的list、set、multiset、map、和multimap迭代器就是这一类迭代器。
- random access迭代器继承自bidirectional迭代器。它厉害的地方在于可以向前或向后跳跃任意距离,这点类似原始指针,内置指针就可以当做random access迭代器使用。vector、deque和string的迭代器就是这类。
这5中分类,C++标准程序库提供专属卷标结构(tag struct)加以确认:
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 {};
在了解了迭代器类型后,我们该去实现advance函数了。实现要高效,对于random access迭代器来说,前进d距离要一步完成,而其他类型则需要反复前进或后退
template<typename Iter, typename DistT> void advance(IteT& iter,DistT d) { if(iter is a random access iterator) iter+=d; else { if(d>=0) while(d--) ++iter; else while(d++) --iter; } }
在上面实现中要判断iter是否为random access迭代器,即要知道IterT类型是否为random access类型。这就需要traits,它允许我们在编译期间获取某些类型信息。traits是一种技术,是C++程序员共同遵守的协议。这个技术要求之一就是,它对内置类型和自定义类型表现的一样好。traits必须能够施行于内置类型,意味着“类型内的嵌套信息”这种东西出局了,因为我们无法将信息嵌套于原值指针内。所以类型的traits信息必须位于类型自身之外。标准技术是把它放进一个template及其一个或多个特化版本中。这样的templates在STL中有若干个,迭代器的为iterator_traits:
template<typename IterT>//用来处理迭代器分类 struct iterator_traits;
虽然iterator_traits是个struct,往往称作traits classes。其运作方式是,针对每一个类型IterT,在struct iterator_traits内声明某个typedef命名为iterator_category,用来确认IterT的迭代器分类。iterator_traits以两个部分实现上述所言。1、它要求用户自定义的迭代器嵌套一个typedef,名为iterator_category,用来确认是哪个卷标结构(tag struct),例如deque和list
template<typename T> class deque{ public: class iterator{ public: typedef random_access_iterator_tag iterator_category; …… }; …… }; template<typename T> class list{ public: class iterator{ public: typedef bidirectional_iterator_tag iterator_category; …… }; …… }; template<typename IterT>//IterT的iterator_category就是用来表现IterT说自己是什么 struct iterator_traits{ //typedef typename的使用,见**条款**42 typedef typename IterT::iterator_category iterator_category; …… };
这样对用户自定义类型行得通,但是对指针行不通,指针也是迭代器,但是指针不能嵌套typedef。下面就是iterator_traits的第2部分了,专门用来支持指针。
为了支持指针迭代器,iterator_traits特别针对类型提供一个偏特化版本(partial template specialization)。
template<typename IterT> struct iterator_traits<IterT*>//针对内置指针 { typedef random_access iterator_tag iterator_category; …… };
现在可以直到实现一个traits class步骤了
- 确认若干我们希望将来可取得的类型相关信息。对于迭代器来首,就是可以取得其分类。
- 为该信息选择一个名称。对于迭代器是iterator_category。
- 提供一个template和一组特化版本,内含你希望支持的类型和相关信息。
现在可以实现一下advance了
template<typename IterT, typename DistT> void advance(IterT& iter,DisT d) { if(typeid(typename std::iterator_traits<IterT>::iterator_category)== typeid(std::random_access_iterator_tag)) …… }
虽然逻辑是正确,但并非是我们想要的,抛开编译问题(**条款**48再说),还有一个更根本的问题:IterT类型在编译期间获知,所以iterator_traits::iterator_category在编译期间确定。但是if语句却是在运行期间核定。可以在编译期间完成的事情推到运行期间,这不仅浪费时间,还造成执行文件膨胀。
要在编译期间确定,可以使用重载。重载是在编译期间确定的,编译器会找到最匹配的函数来调用
template<typename IterT, typename DisT> void doAdvance(IterT& iter, Dist d, std::random_access_iterator_tag) { iter+=d; } template<typename IterT, typename DisT> void doAdvance(IterT& iter, Dist d, std::bidirectional_iterator_tag) { if(d>=0) while(d--) ++iter; else while(d++) --iter; } template<typename IterT, typename DisT> void doAdvance(IterT& iter, Dist d, std::input_iterator_tag) { if(d<0) throw std::out_of_range("Negative distance"); while(d++) --iter; } template<typename IterT,typename DistT> void advance(IterT& iter,DistT d) { doAdvance(iter,d,typename::std::iterator_traits<IterT>::iterator_category(); }
因为forward_iterator_tag继承自input_iterator_tag,所以input_iterator_tag版本的函数可以处理forward迭代器,这是因为public继承是is-a关系。
现在来总结一下如何使用traits class
- 建立一组重载函数或函数模板(例如doAdvance),彼此间差异只在于各自的traits参数。每个函数实现与之接受的traits信息像匹配。
- 建立一个控制函数或函数模板(例如advance),调用上面的函数并传递traits class信息。
Traits广泛应用在STL,除了上面所说的iterator_traits,还有char_traits用来保存字符类型相关信息,numeric_limits用来保存数值类型相关信息。
TR1(**条款**54导入许多新的traits classes用来提供类型信息,例如is_fundamental判断T是否是内置类型,is_array判断T是否为数组,is_base_of
- 《Effective C++》:条款46-条款47
- Effective C++:条款01
- Effective C++:条款02
- Effective C++:条款03
- Effective C++:条款04
- Effective C++:条款05
- Effective C++:条款06
- Effective C++:条款07
- Effective C++:条款08
- 《effective C++》条款三
- 《effective C++》条款5
- 《effective C++》条款六
- Effective C++--经验条款
- Effective C ++ 条款34
- 《Effective C++》条款05
- 《Effective C++》条款06
- 《Effective C++》条款08
- 《Effective C++》资源管理:条款13-条款15
- [C++学习]继承
- ActivityThead ActivityManagerService 和activity关系概述
- R之回归分析
- iOS——UIScrollView的适配问题
- boost asio timer,linux time and timer
- 《Effective C++》:条款46-条款47
- 工作总结第十天
- Java基础(包、多线程)
- 【java学习笔记】application&applet 示例
- MySql 基本操作语句整理
- Makefile浅谈
- R之文本挖掘
- java 网络编程学习笔记 -1
- ubuntu添加用户后无法使用ll且控制台无颜色