《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

0 0