《Effective C++》:条款46-条款47
条款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
版权声明:本文博主原创文章,博客,未经同意不得转载。