条款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的更多相关文章

  1. Effective C++ 条款46

    本节条款:须要类型转换时请为模板定义非成员函数 这节知识是在条款24的基础上,讲述的有关非成员函数在模板类中(non-member function template)的作用. 我们先看一下条款24讲 ...

  2. Effective C++ -----条款46:需要类型转换时请为模板定义非成员函数

    当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”.

  3. 《Effective C++》:条款48:理解力template 元编程

    Template metaprogramming(TMP,模板元编程)这是写template-based C++规划.编译过程.template metaprogramming随着C++写模板程序,化 ...

  4. 《effective C++》:条款37——绝不重新定义继承而来的缺省参数值

    引子: 阿里的一道题: #include <IOSTREAM> using namespace std; class A{ public: ) { cout<<"a~ ...

  5. Effective C++:条款27——条款

    条款27:尽量少做转型动作 单一对象可能拥有一个以上的地址!

  6. 《MORE EFFECTIVE C++》条款20 条款21

    条款20 协助编译器实现返回值优化 当重载运算符的时候,比如+ - * / 这类运算符,该函数返回的值一定是个右值(即不能是引用),那么执行一次运算的开销可能会在临时对象上调用多次构造函数和析构函数, ...

  7. Effective C++ 笔记:条款 32 确定你的public继承塑造出正确的is-a关系

    32 : Make sure public inheritance models "is-a." 0 引言 Inheritance and Object-Oriented Desi ...

  8. Effective C++ 笔记:条款 31 将编译关系降至最低

    31 : Minimize compilation dependencies between files 1 这关乎C++的类(或说都是类惹的祸) 1.1 C++类定义式的问题 C++类定义式不只叙述 ...

  9. Effective C++ 笔记:条款 30 inline

    30 : Understand the ins and outs of inlining 1 inline申请书 1.1 类内部实现函数包含隐藏的inline申请 class Human { publ ...

随机推荐

  1. 修改MySQL 5.5的max_allowed_packet属性的方法

    今天在部署一个实验系统的时候,报出下面这个错: Your 'max_allowed_packet' variable is set to less than 16777216 Byte (16MB). ...

  2. Struts2实现单文件上传

    首先配置一下web.xml <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi ...

  3. jquery第一期:运行第一个jquery

    首先下载js文件,网址jquery.com去下载,可以下载1.10版的 首先打开editplus进行编辑,添加js文件: 编写代码: <!DOCTYPE html PUBLIC "-/ ...

  4. STL之nth_element()(取容器中的第n大值)

    nth_element()函数 头文件:#include<algorithm> 作用:nth_element作用为求第n大的元素,并把它放在第n位置上,下标是从0開始计数的,也就是说求第0 ...

  5. 易语言转C#小试牛刀

    呵呵,用了几年的易语言,太郁闷了,玩过E的童鞋们懂得,偶然机会尝试C#,现正式投入C#门下. 我会把我学习C#的一些知识和重点,实时发不到我的BLOG中,同想学习C#的童鞋一起成长起来.

  6. winform最小化到托盘

    1.拖取NotifyIcon控件.将该控件的visible设成false. 2.指定NotifyIcon的Icon(很重要,否则最小化后找不到窗口). 3.找到window的SizeChanged事件 ...

  7. SQL语言学习-数据操纵语言

    一般而言,数据库中数据的生命周期包括数据插入以及更新.数据删除3个阶段.首先需要用户或者系统将数据插入表.然后,对数据的使用,包括数据的检索以及数据的更新.最后,如果数据已经没有使用价值,则将数据删除 ...

  8. c++对文件操作的支持(一)

    #include <stdio.h> #include <iostream> #include <fstream> using namespace std; voi ...

  9. outlook 2007 IMAP设置和配置

    以Outlook2007为例(Outlook2003操作基本类似).  1.依次点击“工具”>“帐户设置”. 2.在“帐户设置”页中点击“新建”> 不需要做任何选择,点击下一步. 3.填写 ...

  10. ubuntu中vim找不到配色方案blackboard

    在ubuntu下启动vim,提示找不到配色方案blackboard(或其他的), 如何挑选自己喜欢的配色方案呢?在/usr/share/vim/vim72/colors中,(这里根据自己的vim版本选 ...