STL源码分析之literator和triats编程技法

Traits编程技法:

1、 关于模板的参数推导

此例中以func为对外接口,却把实际操作全部放在func_impl中,由于func_impl是一个function template,一旦被调用,编译器会自动进行template参数推导,于是导出型别T,顺利解决问题

 

         迭代器所指对象的型别,称为该迭代器的 value type .上述的参数型别推导虽可用,但不是全面可用。 万一value type必须用于函数的传回值,就束手无策了。因为该法推导的只是参数,而不能用于函数返回值类型

 

         于是尝试使用内嵌型别:

可以看到,通过内嵌型别的声明我们得到了希望的返回类型。

注意的是,func的返回型别必须加上关键词typename,因为T是一个template参数,在它被编译器具体化之前,编译器对T一无所知,换句话说,编译器此时并不知道MyIter<T>::value_type代表的是一个型别或是一个member 或是一个data member。关键词typename告诉编译器这是一个型别,如此才能顺利通过编译

 

         如此一来,看起来不错,但不是所有的迭代器都是class type。比如原生指针。既然不是class type,那就无法为它定义内嵌型别,但我们又必须能接受原生指针,下面看看如何处理。

 

 

偏特化

         如果class template 拥有一个以上的template参数,我们可以针对其中某个(或数个,但不是全部)template参数进行偏特化操作。换句话说,我们可以在泛化中提供一个特化版本(也就是将泛化版本中的template参数赋予明确的指定)

假如有一个 class template如下:

Template<typename U, typename V, typenameT>
Class C{…}

偏特化的字面理解很容易误导

所谓偏特化,其实是提供另一份template的定义式,而起本身仍然是templatized

或者说针对template参数更进一步的条件限制设计出来的特化版本

 

面对下面这个class template

Template<typename  T>
Class C{…};                                            //这个泛化版本允许(接受)T为任何类型

一个偏特化版本是:

Template<typename T>
Class C<T*>{…};

这个特化版本仅适用于“T为原生指针”的情况

“T为原生指针”即为“T为任何类型”的更进一步的限制

 

 

于是,我们就可以解决上面遇到的问题了。

现在,我们可以针对“迭代器的template参数为指针”者设计特化版的迭代器

下面这个class template是专门用来“萃取”迭代器的特性,value type正是迭代器的特性之一

Template<class I>
Struct iterator_traits{
         typedef typename I::value_type value_type;
}

大意是,如果I定义有自己的value_type, 那么通过这个trait的作用,萃取出来的value_type就是I::value_type。

换句话说,如果I定义有自己的value_type,先前那个func就可以写成这样:

Template<class I>
Typename iterator_traits<I>::value_type
Func(Iite)
{
    Return *ite;
}


与之前的相比较,是多了一层间接性,好处却大大增加了。因为trait可以拥有特化版本,如下,我们新增特化版本:

Template<class T>
struct iterator_traits<T*>{
        typedef T value_type;.
}

比如传入的是int *类型的,那么value_type就是int 类型的了

于是,虽然原生指针不是class type,但我们依旧处理了他。

 

针对“指向常数对象的指针”,比如 iterator_traits<const int *>::value_type得到的类型是const int 类型而非int ,这是无法赋值的暂时变量,毫无作用,因此假如传入的是这种类型的参数,我们应该将其变为non-const类型的,于是

Template<class T>
Struct iterator_traits<const T*>{
         typedef T value_type;                        //于是萃取出来的就是T了而非const T
}


 

最常用的迭代器型别有以下五种,若希望开发的容器与STL交融,那么就一定要为容器定义下列型别:

template <class Iterator>
struct iterator_traits {
  typedef typename Iterator::iterator_category iterator_category;
  typedef typename Iterator::value_type        value_type;
  typedef typename Iterator::difference_type   difference_type;
  typedef typename Iterator::pointer           pointer;
  typedef typename Iterator::reference         reference;
}



我们来深入讨论迭代器的相应型别:

 

1、value_type

即容器中的 typedef T value_type这句话

指的是迭代器所指对象的型别

 

 

2、difference type

表示两个迭代器之间的距离,它也可以用来表示一个容器的最大容量,因为对于连续空间而言,头尾之间的距离就是其最大容量

提供计数功能的函数count,其传回值就必须使用differnce type

template <class InputIterator, class T>
typename iterator_traits<InputIterator>::difference_type
count(InputIteratorfirst, InputIterator last, const T& value) {
  typename iterator_traits<InputIterator>::difference_type n= 0;
  for ( ; first != last; ++first)
    if (*first == value)
      ++n;
  return n;
}

针对原生指针,则使用内建类型:typedef ptrdiff_t                  difference_type;

       

 

5、iterator_category

C++中迭代器被分为五类:

Input Iterator:

    不允许外界改变,只读(read only)

Output Iterator

    只写(write only)

Forward Iterator

         只能往前进,允许写入型算法,在此种迭代器形成的区间上进行读写操作

Bidirectional Iterator

    可双向移动

RandomAccess Iterator

         前三种迭代器支持operator++, 第四种迭代器加上支持operator--, 这最后一种涵盖所有算数能力,包括 p+n, p-n, p[n] ,p1-p2, p1<p2

 

强化关系如下:

根据这里的关系,书上提到,在研究STL的过程中,效率永远是重要课题。假设有个算法可以接受Forward Iterator,但是传入的是RandomAccess Iterator,它当然也能接受,但是可用并不代表最佳!

 

拿advance举例,以下并不是源码,是我们自己的针对不同类型迭代器的函数:

template <class InputIterator, class Distance>
inline void__advance_II(InputIterator& i, Distance n) {
  while (n--) ++i;
}

template <classBidirectionalIterator, class Distance>
inline void __advance_BI(BidirectionalIterator&i, Distance n) {
  if (n >= 0)
    while (n--) ++i;
  else
    while (n++) --i;
}

template <class RandomAccessIterator,class Distance>
inline void __advance_RI(RandomAccessIterator&i, Distance n) {
  i += n;
}


 

对于三种不同的迭代器(说是这么说,但其实三个函数类型是相同的,都是template,等待将来的输入决定),分别采用了不同的算法,算法就不解释了,一眼就看明白了

当程序调用advance的时候,应该选用哪种方法呢?对于不同的迭代器,如果调用不匹配的算法,那么可能造成错误或者效率大减,下面是一种处理方法:

 

template <class InputIterator, class Distance>
voidadvance(InputIterator i, Distance n)
{
    If(is_random_access_iterator(i))
        advance_RAI();
    else if (is_bidirectional_iterator(i))
        advance_BI(i);
    else
        advance_II(I, n)
}


不过,此种在运行时才决定使用哪个版本会影响后序的效率,最好是能够在编译器就决定使用哪个版本,重载可以解决这个问题

这里谈到的三个算法函数都有两个参数,型别都是未定的,无法成为重载函数。为了令其同名形成重载函数,我们必须加上一个型别已经确定的函数参数,使他们重载

 

设计如下考虑:

         如果traits有能力萃取出迭代器的种类,我们便可以利用此类型作为每个advance算法的第三个参数。这个型别必须是一个class type, 不能只是数字之类,因为编译器需要仰赖他们进行重载决议

下面是五种迭代器的型别:

struct input_iterator_tag{};
struct output_iterator_tag{};
structforward_iterator_tag : public input_iterator_tag {};
structbidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag: publicbidirectional_iterator_tag {};

这些都是用作标记的,不需要内容,只是为何继承以后在谈到。

 

经过添加第三个参数,且处理成为重载,三个算法变为了如下所示:

template <class InputIterator, class Distance>
inline void__advance(InputIterator& i, Distance n, input_iterator_tag) {
  while (n--) ++i;
}
 

template <classBidirectionalIterator, class Distance>
inline void__advance(BidirectionalIterator& i, Distance n,
                     bidirectional_iterator_tag) {
  if (n >= 0)
    while (n--) ++i;
  else
    while (n++) --i;
}

template <classRandomAccessIterator, class Distance>
inline void__advance(RandomAccessIterator& i, Distance n,
                     random_access_iterator_tag) {
  i += n;
}


我们看到第三个参数只声明参数的类型,因为函数根本不使用该参数,只是纯粹用来激活重载机制,加名称也只是画蛇添足

 

好了,重载完成,我们还需要一个开放的接口给用户,且能将迭代器的型别判定出来可以传递给__advance函数,进行重载判定。迭代器类型的判断就交给traits

 

template <class InputIterator, class Distance>
inline voidadvance(InputIterator& i, Distance n) {
  __advance(i, n,iterator_traits<InputIterator>::iterator_category());
}

值得注意的是,这里写的是iterator_traits<InputIterator>::iterator_category(),这里我就产生了一些疑惑,可能是一些知识缺乏的问题。于是我打开stl_list文件,查看其中关于iterator_category的定义,发现typedefrandom_access_iterator_tag iterator_category;

于是发现,iterator_traits<InputIterator>::iterator_category()其实就是返回了一个iterator_category的一个实体对象,下面看个简短的例子:

#include <iostream>
using namespace std;
 
void fun(int x)
{
    cout<< x <<endl;
}
 
int main()
{
    fun(int(10));
    return0;
}

这个程序成功的输出了10.

 


在STL源码中,其自行定义了一个函数,而不是像这里一长条:

我们要注意的一个地方是,在这个接口函数的template中,未知的情况下声明的是class InputIterator,这事因为STL的默认规定,将其声明为算法所能接受的最低阶迭代器类型

template <class InputIterator, class Distance>
inline voidadvance(InputIterator& i, Distance n) {
  __advance(i, n, iterator_category(i));
}
 
template <class Iterator>
inline typenameiterator_traits<Iterator>::iterator_category
iterator_category(const Iterator&) {
  typedef typename iterator_traits<Iterator>::iterator_categorycategory;
  return category();
}

另外,对于原生指针,使用的都是random access iterator,这个不用解释

 

 

接下来,我们来解决之前遇到的疑问,为什么XXX_iterator_tag之间存在继承关系?

如下:

 

首先我们需要看看struct之间的继承例子:

#include <iostream>
using namespace std;
 
struct B {};
struct X:public B{};
struct Y:public B{};
 
template <class K>
void func(K&p, B)
{
    cout<< "B version " << endl;
}
template <class K>
void func(K&p, X)
{
    cout<< "X version " << endl;
}
 
int main()
{
    intp;
    func(p,B());               //输出B version
    func(p,X());               //输出X version
    func(p,Y());               //输出B version
    return0;
}


 

看到这里,我想应该对为什么使用继承有所了解了。

这是因为在算法定义的时候,某算法可能定义了InputIterator,BidirectionalIterator, RandomAccessIterator,但我们传入的是ForwardIterator,于是根据重载和继承的原则,程序就会去调用InputIterator的算法定义了

可见,继承可以避免重复的算法定义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值