引言
在STL的设计中,对不同的容器(Container)进行某种处理往往使用的都是同一个函数,比如要将一个整型链表(std::list
)或者数组里的元素划分成大于100和小于等于100两部分,直接调用std::partition()
即可,非常方便。
迭代器
为了达到这样的效果,STL中所有容器都包含了迭代器(Iterator),都可以通过迭代器对其进行访问。正如《STL源码剖析》中所说的,迭代器就像胶水一样,把STL中的容器和算法粘合到一起。
但是针对不同容器进行处理,往往要根据容器自身数据结构的特点使用不同的算法才能使效率最大化,而STL中算法函数的传入参数都是容器的迭代器,那是不是每个算法的实现都要针对每一种容器的迭代器重载一遍呢?
当然不可能,如果是这样STL的代码量就远不止现在这么点了。为了方便算法实现,设计者们将容器抽象为几类:
- 可读,支持单向单趟访问
- 支持单向多趟访问
- 支持双向访问
- 支持随机访问
相应地,他们的迭代器也就分为:
- 输入迭代器
- 前向迭代器
- 双向迭代器
- 随机访问迭代器
上面每一个迭代器都在前一个的基础上支持更多的功能。
(当然实际上还有所谓的“输出迭代器”,这里不作讨论。)
那么问题来了,STL怎么根据不同的迭代器类型调用不同的函数的呢(即如何进行分发)?
最直接的做法是在每一个迭代器类中定义一个静态变量,保存其所属的迭代器类型。函数通过传入的迭代器访问该变量获知其类型,通过条件判断使用相应的算法来处理。
不考虑其它的副作用,实际上这么做是没有必要的,因为迭代器的类型在编译的时候就已经是确定了的,把编译期能做的事情推到运行期,这不是C++的风格。
下面看一下STL中是怎么处理这个问题的。
tag
对于STL中每一种迭代器,都有一个表明其所属类型的tag。这些tag的定义如下:
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 {};
可以很清楚地看到他们的继承关系
在每一个迭代器类中,以typedef
关键字定义其所属迭代器种类iterator_category
比如这样:
class xxx_iterator
{
public:
//省略无关代码
typedef bidirectional_iterator_tag iterator_category;
};
所以如果你需要实现自己的支持STL 的容器,也必须这样定义一个迭代器。
iterator_traits
那么现在问题就变成了如何将迭代器类中的iterator_category
类型提取出来?
这里需要借助一个辅助工具类iterator_traits
,其定义如下:
template <class Iterator>
struct iterator_traits<Iterator>
{
//省略无关代码
typedef typename Iterator::iterator_category iterator_category;
};
可能有人会问,明明可以直接xxx_iterator::iterator_category
得到它的类型,为什么非要多套一层iterator_traits
来获取呢?
别忘了STL不仅能对容器进行操作,它还支持原生数组。原生数组只是一段连续的内存,其中的元素通过指针访问。虽然数组指针可以被视为一种随机访问迭代器,但是它不是一个具体的迭代器类,自然也就没法定义它的iterator_category
。
而通过iterator_traits
,就可以利用模板偏特化为指针提供iterator_category
template<class _Tp>
struct iterator_traits<_Tp*>
{
//省略无关代码
typedef random_access_iterator_tag iterator_category;
};
重载分发
在获得迭代器的iterator_category
后又该如何判断其iterator_category
然后调用相应的代码呢?
iterator_category
不是变量,C++中也没有if (type(xxx) == bidirectional_iterator_tag)
这样的特性。
但是C++支持函数重载,即允许同名但是参数列表不同的函数存在。在函数调用时传入不同类型的参数就可以调用不同的函数。以libc++中std::rotate()
的代码为例:
template <class _ForwardIterator>
_ForwardIterator
__rotate(_ForwardIterator first, _ForwardIterator middle, _ForwardIterator last,
forward_iterator_tag)
{
//针对前向迭代器优化的算法
}
template <class _BidirectionalIterator>
_BidirectionalIterator
__rotate(_BidirectionalIterator first, _BidirectionalIterator middle, _BidirectionalIterator last,
bidirectional_iterator_tag)
{
//针对双向迭代器优化的算法
}
template <class _RandomAccessIterator>
_RandomAccessIterator
__rotate(_RandomAccessIterator first, _RandomAccessIterator middle, _RandomAccessIterator last,
random_access_iterator_tag)
{
//针对随机访问迭代器优化的算法
}
template <class _ForwardIterator>
_ForwardIterator
rotate(_ForwardIterator first, _ForwardIterator middle, _ForwardIterator last)
{
//省略无关代码
return __rotate(first, middle, last,
typename iterator_traits<_ForwardIterator>::iterator_category());
}
它通过传入一个iterator_category
类型的空对象从而调用对应的重载函数,最终实现了基于标签的分发。
简单总结
记得曾有人说过,C++中的模板就是尖括号版的Lisp,是一个图灵完备的函数式编程语言。文中说的Tag Dispatching实际上就是一种运行在编译期的条件分支。