《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

版权声明:本文博主原创文章,博客,未经同意不得转载。

posted @ 2015-09-26 13:18  hrhguanli  阅读(197)  评论(0编辑  收藏  举报