迭代器设计理念 - STL关键所在

STL的中心思想在于,将容器(container)和算法(algorithm)分开,彼此独立设计,最后再用粘合剂粘合到一起,而这个
粘合剂就是iterator(迭代器)

下面从stl_algo.h截取一下容器、算法、迭代器合作的展示,以find()为例,它接受两个迭代器和一个目标

template <class _InputIter, class _Tp>
inline _InputIter find(_InputIter __first, _InputIter __last,
                       const _Tp &__val,
                       input_iterator_tag)
{
    while (__first != __last && !(*__first == __val))
        ++__first;
    return __first;
}

// 结合容器使用
int ia[7] = {0, 1, 2, 3, 4, 5, 6};
vector<int> v(ia, ia + 7);
list<int> l(ia, ia + 7);
deque<int> dq(ia, ia + 7);

vector<int>::iterator it1 = find(v.begin(), v.end(), 4);
list<int>::iterator it2 = find(l.begin(), l.end(), 4);
deque<int>::iterator it3 = find(dq.begin(), dq.end(), 4);

从上面的例子可以看出来,不同类型的容器,可以通过迭代器使用相同的算法进行处理,这就是迭代器设计精妙所在

迭代器是一种smart-pointer

迭代器是一种行为类似于指针的对象,而原生指针的主要工作就是内容提领(解引用)和成员访问,所以迭代器的首要目标
是重载operator*() 和 operator->(),关于这一点,C++标准库中auto_ptr(不过已经废弃,被unique_ptr代替)
可以提供参考,下面为auto_ptr实现源码

template <class _Tp>
class auto_ptr
{
private:
    _Tp *_M_ptr;

public:
    typedef _Tp element_type;

    // explicit: 禁止隐式类型转换,即禁止auto_ptr<int> p = new int(10)这种语法
    // 因为上述语句会转化为auto_ptr<int> p = auto_ptr<int>(new int(10));
    // 为什么禁止隐式类型转换?因为会导致所有权转移
    // auto_ptr<int> p1(new int(10));
    // auto_ptr<int> p2 = p1;
    // 执行后对象所有者就会从p1转为p2
    // 如果支持隐式类型转换,在下面这段代码执行结束后,对象就会从p转移到函数参数中
    // void func(auto_ptr<int> p);
    // auto_ptr<int> p(new int(10));
    // func(p);  // p 被偷偷 transfer ownership

    explicit auto_ptr(_Tp *__p = 0) __STL_NOTHROW : _M_ptr(__p) {}
    auto_ptr(auto_ptr &__a) __STL_NOTHROW : _M_ptr(__a.release()) {}

    auto_ptr &operator=(auto_ptr &__a) __STL_NOTHROW
    {
        if (&__a != this)
        {
            delete _M_ptr;
            _M_ptr = __a.release();
        }
        return *this;
    }

    ~auto_ptr() { delete _M_ptr; }

    _Tp &operator*() const __STL_NOTHROW // 主要重载了* 和->这两个运算符
    {
        return *_M_ptr;
    }
    _Tp *operator->() const __STL_NOTHROW
    {
        return _M_ptr;
    }
    _Tp *get() const __STL_NOTHROW
    {
        return _M_ptr;
    }
    _Tp *release() __STL_NOTHROW
    {
        _Tp *__tmp = _M_ptr;
        _M_ptr = 0;
        return __tmp;
    }
    void reset(_Tp *__p = 0) __STL_NOTHROW
    {
        if (__p != _M_ptr)
        {
            delete _M_ptr;
            _M_ptr = __p;
        }
    }
};

迭代器的内置型别

在算法中运用迭代器的时候,很可能会用到相应型别,那什么是相应型别?迭代器所指之物就是其一。假如算法中有必要
声明一个变量,以"迭代器所指对象的型别"为型别,怎么办呢?C++只有sizeof()并没有typeof()。这里给出一个解决方
法->使用function template的参数推导机制,在模版实例化的时候,我们就可以知道它是什么类型了

template<class I, class T>
void func_impl(I iter, T t)
{
    T tmp = t; // 在这里解决问题,T就是我们想知道的迭代器所指之物的类型

    // ... 这里做func原本要做的事
}

template<class I>
void func(I iter)
{
    func_impl(iter, *iter);
}

int main()
{
    int i = 0;
    func(&i);
}

上面这段demo可以很好的展现出怎么通过模版的参数推导完成类型判断的,但是问题来了,所有的迭代器的内置型别都可以
用上述模版参数推导来取得吗?答案是不可以的,所以我们需要更全面的解法

在这里,为了防止读者不理解“迭代器的内置型别”究竟是何物,我将列出常见的五种迭代器的内置型别

  1. value_type (上文所说迭代器所指之物)
  2. differnce_type (两个迭代器之间的距离)
  3. pointer (指针 *)
  4. reference (引用 ->)
  5. iterator catagoly(迭代器类型)
    至于他们具体是什么,请继续往下看

Traits编程技法 - STL源代码之钥

迭代器所指对象型别,称为value_type,上述通过模版参数推导机制获得value_type的方法并不能全面覆盖,如果
value_type必须用为函数的传回值,就无法使用了,因为模版类型推导只能作用于参数,无法推导返回值类别

那我们怎么办呢? STL的大佬想出一种方式 -> 在iterator的定义内部,内嵌型别声明(nested type)
在这里截出一段input_iterator(在这里可以先把它看作普通的iterator,后面会细说)的源码可以观察到,
SGI-STL的实现确实就是这样的

template <class _Tp, class _Distance>
struct input_iterator
{
    typedef input_iterator_tag      iterator_category;
    typedef _Tp                     value_type;
    typedef _Distance               difference_type;
    typedef _Tp *                   pointer;
    typedef _Tp &                   reference;
};

// 搭配源码我自己补充一段,体现类型的获取过程
template<class I>
typename I::value_type value_type // 这里一行都是返回值类型
func(I it)
{   
    return *it;
}

注意,在使用I::value_type的时候必须要加上typename的声明,因为编译器不知道input_iterator<_Tp>在实例
化前到底是什么,typename的作用是告诉编译器这是一个类型

看起来这个方式确实不错,但是这里面有一个隐晦的陷阱,即只有class type才有内嵌类别,但并不是所有的迭代器都是
class type,原生指针就不是!!!(int*, double*, char* 等),但是迭代器必须要接受原生指针,这时,我们可以
想到一种方式template partial specializtion(模版的偏特化)

partial specializtion(偏特化)的意义

在讲解这小节之前,我们先来看C++提供的对模版的两种特化方式

// 普通泛化版本(template)
template<class T>
struct Printer
{
    void print()
    {
        std::cout << "generic type" << std::endl;
    }
};

// 1. 全特化: 使用者想在使用某种类型的时候,使用特别的Print函数
template<>
struct Printer<int> // 特化类型为int
{
    void print()
    {
        std::cout << "int type" << std::endl;
    }
};
// 当其他类型调用这个函数的时候,会打印generic type,而int类型则会输出int type

// 2. 偏特化: 当使用者想在使用某一大类的时候(如*, const *类型)
template<class T>
struct iterator_traits<T*>
{
    typedef T value_type;
};

// 这时候就是对指针类型的一种偏特化

在了解上述特化机制后,我们就可以明白为什么STL要使用偏特化的方法来解决原生指针没有class的难题
现在我们就可以对迭代器参数为指针者设计特化版的迭代器

注意,下面这种class template转门用来"萃取"迭代器类型的特性

template<class I>
struct iterator_traits{ // traits意为特质
    typedef typename I::value_type value_type; 
}

所谓的traits,就是当迭代器I有自己的value_type类型的时候就会通过上面这个iterator_traits萃取出来
有了这个,上面的func函数就可以被改写

// 原func函数
// template<class I>
// typename I::value_type value_type // 这里一行都是返回值类型
// func(I it)
// {   
//     return *it;
// }

template <class I>
typename iterator_traits<I>::value_type
func(I it)
{
    return *it;
}

这时有人会问,多包一层的意义在哪?这时偏特化就该登场了!!!

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

经过这一层偏特化,原生指针int* 虽然没有class type,但是也可以通过iterator_traits萃取出value_type
但是如果传入的是const int* 呢?得到的是const int,我们只是想知道运算的类型是什么,所以得到const int
并没有什么用,那就再将const T*偏特化一下!!!

template<class T>
struct iterator_traits<const T*>{
    typedef T value_type;
}

这时不管我们传入的是const int* 还是int*,我们都可以获得对应的value_type.

五种迭代器相应型别

除了value_type之外,我们在上面说了,迭代器的相应型别还有differnce_type, pointer, reference,
iterator_category,这里贴出文件stl_iterator_base中iterator_traits的实现及其特化版本

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;
};

// pointer特化
template <class _Tp>
struct iterator_traits<_Tp*> {
  typedef random_access_iterator_tag iterator_category;
  typedef _Tp                         value_type;
  typedef ptrdiff_t                   difference_type;
  typedef _Tp*                        pointer;
  typedef _Tp&                        reference;
};

// pointer to const特化
template <class _Tp>
struct iterator_traits<const _Tp*> {
  typedef random_access_iterator_tag iterator_category;
  typedef _Tp                         value_type;
  typedef ptrdiff_t                   difference_type;
  typedef const _Tp*                  pointer;
  typedef const _Tp&                  reference;
};
  • value_type
    上面大量说的"迭代器所指之物"的类型,就是value_type

  • difference_type
    用来代表两个迭代器的距离,如果一个STL的泛型算法想提供计数功能,如count(),它的返回值应该是difference_type
    stl_algo文件中count()实现如下

template <class _InputIter, class _Tp>
typename iterator_traits<_InputIter>::difference_type
count(_InputIter __first, _InputIter __last, const _Tp &__value)
{
    __STL_REQUIRES(_InputIter, _InputIterator);
    __STL_REQUIRES(typename iterator_traits<_InputIter>::value_type,
                   _EqualityComparable);
    __STL_REQUIRES(_Tp, _EqualityComparable);
    typename iterator_traits<_InputIter>::difference_type __n = 0;
    for (; __first != __last; ++__first)
        if (*__first == __value)
            ++__n;
    return __n;
}
  • reference type
    要理解这个reference type,我们要先知道C++中的左值和右值分别代表什么,
    C++中的左值/右值本质要解决:对象的生命周期 + 是否可以被修改 + 是否可以被绑定引用
    左值: 生命周期稳定,可以被修改,可以被绑定引用
    右值: 生命周期短(表达式结束后就销毁),不可以被修改,不可以绑定引用
    举例子来看
int a = 10;
// 在上述表达式中,a就是左值(有名字,有地址,可以修改),而10就是右值,不可以修改,表达式结束后销毁
// &a是合法的,而&10则不可以

int a = 10;
int& r = a;
// 上面就是左值引用,a是一个左值,而int& r = 10则是不被允许的,因为10是一个右值,表达是结束后会被释放

const int& r = 10;
// 但是这个是被允许的,因为编译器会根据10来生成一个临时变量
// 即const int temp = 10; const int& r = temp;

总结一下:
左值可以看作一个对象,右值则代表一个值
T& 只允许绑定左值,const T& 允许绑定左值和右值

有了以上知识,我们可以得知: 在C++ 中,函数如果要传回左值,都是以 by reference 的方式进行,所以当P是个 mutable iterators 时,如果其 value type 是T,那么*p的型别不应该是T,应该是T&。将此道理扩充,如果p是一个 constant iterators,其 value type 是T,那么 *p 的型别不应该是 const T,而应该是 const T&。

// 假如我们有这个迭代器了,我们想得到它的值,我们就会这样使用*p
vector<int>::iterator p;
*p;
// 但是*p是什么类型呢?int?int&?举个例子
vector<int> v = {1,2,3};
vector<int>::iterator p = v.begin();
*p = 10;
// 这是正确的语法,如果*p的类型是int的话,根据我们上面左右值的讲解会得到下面的结论
// 1 = 10,这是在给临时对象赋值,是不被允许的,所以很显然*p返回值是int& 即必须是一个左值也就是T&(reference type)
  • pointer type
    与reference type相似,reference type代表的是*p的返回值类型,而pointer type代表的是p->的返回类型
vector<int>::iterator it = v.begin();

*it      // 解引用
it->xxx  // 成员访问
// it->member实际上会被编译器解释为(*it).member
  • iterator category
    根据移动特性和实施操作,iterator被分为五类
  1. input iterator 仅读
  2. output iterator 仅写
  3. forward iterator 可读写,但是只能单向移动
  4. bidirection iterator 可以双向移动
  5. random access iterator 涵盖上面所有能力,并加上p + n, p - n, p1 - p2, p1 + p2的能力

可能会有读者疑问,为什么要有这些类型的迭代器,都设置成相同不好吗?大错特错,迭代器设计的时候需要考虑的是设计适当的相应型别,而迭代器怎么设计是根据不同容器来的(我很喜欢)
不同容器为了实现一些特性肯定要使用不同特点的迭代器,那么设计不同的迭代器类型又有什么用呢?这样设计的原因是在设计算法的时候,我们会尽量根据不同迭代器类型明确一个定义(类似特化)

举个例子,如果一个算法可以接受input iterator那么这个算法肯定也可以接收random access iterator,但是可用并不代表最佳!!!

以advanced()为例

advance()有两个参数,一个是迭代器p,另一个是n,它的的作用是将迭代器p向后移动n次(也有可能是后退),下面有三份定义,分别针对input iterator、bidirection iterator、
random access iterator(至于为什么不设计forward iterator则是因为它的实现与input iterator相同,这里记住,后面要考)

// input iterator
template <class InputIterator, class Distance>
void advance_II(InputIterator it, Distance n)
{
    while(n--)
        ++it; // 注意这里前置++效率比后置要高
}

// bidirection iterator
template <class BidirectionIterator, class Distance>
void advance_BI(BidirectionIterator it, Distance n)
{
    if(n >= 0)
        while(n--) ++it;
    else
        while(n++) --it;
}

// random access iterator
template <class RandomAccessIterator, class Distance>
void advance_RAI(RandomAccessIterator it, Distance n)
{
    it += n
}

现在,当程序调用advance的时候,我们应该选择哪份函数呢?如果选择advance_II()的话,那么RandomAccessIterator的时间复杂度将会从O(1)退化到O(N)
如果选择advance_RAI()呢?那么InputIterator则无法使用这个函数,我们需要将它们三合一,有人肯定想到用if - else判断iterator类型,再分别判断
但是那样会损失效率,最好要在编译阶段我们就知道要选择哪个函数。

这时我们应该想到一个C++重要特性,函数重载!!!,上述函数前两个参数都是不确定的,所以我们需要一个确定的第三个参数来构成函数重载
这也是迭代器设计五种迭代器类型的原因,通过之前说过的traits机制,我们可以很轻松的萃取出迭代器的类型,下面给出源码中迭代器类型标记

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 {};

这些class没有任何成员,只用作标记,这里直接贴出advance的源码实现了。

// 因为__advance只在内部使用所以给它加上前导符

// input iterator实现
template <class _InputIter, class _Distance>
inline void __advance(_InputIter &__i, _Distance __n, input_iterator_tag)
{
    while (__n--)
        ++__i;
}

// bidirection iterator实现
template <class _BidirectionalIterator, class _Distance>
inline void __advance(_BidirectionalIterator &__i, _Distance __n,
                      bidirectional_iterator_tag)
{
    __STL_REQUIRES(_BidirectionalIterator, _BidirectionalIterator);
    if (__n >= 0)
        while (__n--)
            ++__i;
    else
        while (__n++)
            --__i;
}

// random access iterator实现
template <class _RandomAccessIterator, class _Distance>
inline void __advance(_RandomAccessIterator &__i, _Distance __n,
                      random_access_iterator_tag)
{
    __STL_REQUIRES(_RandomAccessIterator, _RandomAccessIterator);
    __i += __n;
}

template <class _InputIterator, class _Distance>
inline void advance(_InputIterator &__i, _Distance __n)
{
    __STL_REQUIRES(_InputIterator, _InputIterator);
    __advance(__i, __n, iterator_category(__i));
}

细心的读者可能已经发现了,上面的__advance()实现中,没有重载forward iterator类型的,难道是源码作者忘掉了?
而实际上,在之前就已经说过,input iterator与forward iterator的实现是相同的,这里我自己杜撰一下如果有
forward iterator重载版本的__advance()将会是什么样子的

template <class _ForwardIter, class _Distance>
inline void __advance(_ForwardIter &__i, _Distance __n, forward_iterator_tag)
{
    __advanc(__i, __n, input_iterator_tag);
}

我们发现这只是一种单纯的传递调用函数,这样会有损性能,违背了STL的初心,这时之前所说的继承,就是在这里起的作用

struct input_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {}; 
struct bidirectional_iterator_tag : public forward_iterator_tag {};

// 为了更好的实验效果,我们将 input_iterator_tag 比喻为B,forward_iterator_tag比喻为D1,bidirectional_iterator_tag比喻为D2
struct B { };
struct D1 : public B { };
struct D2 : public D1 { };

template <class T
void func(I& p, B)
{
    cout << "B version" << endl;
}

template <class T
void func(I& p, D2)
{
    cout << "D2 version" << endl;
}

int main()
{
    int* p;
    func(p, B()); // 这里的B,D1,D2都是临时对象,仅为了观察实验效果
    func(p, D1());
    func(p, D2());
}

// 经实验,func(p, B()); 与参数完全匹配,输出B version,
// func(p, D1()); 与参数未能完全吻合,但是因为继承关系会自动传递调用,输出B version,
// func(p, D2()); 与参数完全吻合,输出D2 version

将上述实验代码复制运行后,我们会神奇的发现D1()的重载函数不存在的时候会自动传递调用父类的对应函数
这也是__advance没有forward iterator版本的原因

回归到advance函数,上层调用的时候,肯定只能传两个参数,那么第三个参数怎么办?很显然,交给traits即可!!!
为了方便获取迭代器内置类型,SGI还特意将iterator_traits封装成几个函数,可以通过调用直接返回类型

template <class _Tp, class _Distance>
inline input_iterator_tag
iterator_category(const input_iterator<_Tp, _Distance> &)
{
    return input_iterator_tag();
}

inline output_iterator_tag iterator_category(const output_iterator &)
{
    return output_iterator_tag();
}

template <class _Tp, class _Distance>
inline forward_iterator_tag
iterator_category(const forward_iterator<_Tp, _Distance> &)
{
    return forward_iterator_tag();
}

template <class _Tp, class _Distance>
inline bidirectional_iterator_tag
iterator_category(const bidirectional_iterator<_Tp, _Distance> &)
{
    return bidirectional_iterator_tag();
}

template <class _Tp, class _Distance>
inline random_access_iterator_tag
iterator_category(const random_access_iterator<_Tp, _Distance> &)
{
    return random_access_iterator_tag();
}

template <class _Tp>
inline random_access_iterator_tag iterator_category(const _Tp *)
{
    return random_access_iterator_tag();
}

template <class _Tp, class _Distance>
inline _Tp *value_type(const input_iterator<_Tp, _Distance> &)
{
    return (_Tp *)(0);
}

template <class _Tp, class _Distance>
inline _Tp *value_type(const forward_iterator<_Tp, _Distance> &)
{
    return (_Tp *)(0);
}

template <class _Tp, class _Distance>
inline _Tp *value_type(const bidirectional_iterator<_Tp, _Distance> &)
{
    return (_Tp *)(0);
}

template <class _Tp, class _Distance>
inline _Tp *value_type(const random_access_iterator<_Tp, _Distance> &)
{
    return (_Tp *)(0);
}

template <class _Tp>
inline _Tp *value_type(const _Tp *) { return (_Tp *)(0); }

template <class _Tp, class _Distance>
inline _Distance *distance_type(const input_iterator<_Tp, _Distance> &)
{
    return (_Distance *)(0);
}

template <class _Tp, class _Distance>
inline _Distance *distance_type(const forward_iterator<_Tp, _Distance> &)
{
    return (_Distance *)(0);
}

template <class _Tp, class _Distance>
inline _Distance *
distance_type(const bidirectional_iterator<_Tp, _Distance> &)
{
    return (_Distance *)(0);
}

template <class _Tp, class _Distance>
inline _Distance *
distance_type(const random_access_iterator<_Tp, _Distance> &)
{
    return (_Distance *)(0);
}

template <class _Tp>
inline ptrdiff_t *distance_type(const _Tp *) { return (ptrdiff_t *)(0); }

上述函数经过封装后可以通过简单的iterator_category(),value_type(),distance_type()调用
在advance()就有体现这点

除此之外,我们发现advance的template中写的是InoutIter,其实这是STL的规定,以算法能接受的
最低阶的迭代器类型,来为其迭代器类型参数命名

以distance()为例

与advance()相似,distance()也是一个使用频率极高的算法,用来计算两个迭代器的距离,设计模式与advance()类似
这里贴出源码,剩余不过多赘述

template <class _InputIterator, class _Distance>
inline void __distance(_InputIterator __first, _InputIterator __last,
                       _Distance &__n, input_iterator_tag)
{
    while (__first != __last)
    {
        ++__first;
        ++__n;
    }
}

template <class _RandomAccessIterator, class _Distance>
inline void __distance(_RandomAccessIterator __first,
                       _RandomAccessIterator __last,
                       _Distance &__n, random_access_iterator_tag)
{
    __STL_REQUIRES(_RandomAccessIterator, _RandomAccessIterator);
    __n += __last - __first;
}

template <class _InputIterator, class _Distance>
inline void distance(_InputIterator __first,
                     _InputIterator __last, _Distance &__n)
{
    __STL_REQUIRES(_InputIterator, _InputIterator);
    __distance(__first, __last, __n, iterator_category(__first));
}

std::iterator的保证

为了保证规范,任何iterator都应该有五种内嵌类型,以便于traits进行萃取,为了防止遗漏
STL提供了iterators class,如果新设计的迭代器都继承于它,则保证符合规范

template <class _Category, class _Tp, class _Distance = ptrdiff_t,
          class _Pointer = _Tp*, class _Reference = _Tp&>
struct iterator {
  typedef _Category  iterator_category;
  typedef _Tp        value_type;
  typedef _Distance  difference_type;
  typedef _Pointer   pointer;
  typedef _Reference reference;
};

如果我们自己要设计迭代器,一般只需要填前两个参数即可(后三个给缺省了)

总结

设计适当的相应型别(associated types),是迭代器的责任。设计适当的选代器,则是容器的责任。
唯容器本身,才知道该设计出怎样的迭代器来遍历自己,并执行迭代器该有的各种行为(前进、后退、取值、取用成员)
至于算法,完全可以独立于容器和迭代器之外自行发展,只要设计时以迭代器对外接口就行。

traits编程技法大量运用于STL实现品中。它利用“内嵌型别”的编程技巧与编译器的 template 参数推导功能,
增强C++未能提供的关于型别认证方面的能力,弥补C++ 不为强型别(strong typea)语言的遗憾。
了解 traits 编程技法,就像获得“芝麻开门”的口诀一样,从此得以一窥STL源代码堂奥。

— 节选自侯捷《STL源码剖析》

事实上,SGI STL不止在迭代器中使用了traits,它更是单独抽出了一个__type_traits,它可以负责萃取别的类型
如果读者以后有时间,可以单独再看一下type_traits.h这个文件中的__type_traits,相信你肯定会有更多收获

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐