分类: C/C++
2009-09-19 12:02:02
正如你所知的,每个标准容器类都提供四种迭代器类型。对于container
让我向你演示两个东西。第一,看看vector
iterator insert(iterator position, const T& x);
iterator erase(iterator position);
iterator erase(iterator rangeBegin, iterator rangeEnd);
每个标准容器都包含了和这差不多的函数,虽然返回类型因容器类型的不同而不同。需要注意的是:这些方法只接受iterator类型的参数,而不是const_iterator、reverse_iterator或const_reverse_iterator。总是iterator。虽然容器类支持四种迭代器类型,但其中的一种类型有着其他所没有的特权。那就是iterator,iterator比较特殊[2]。
我要让你看的第二个东西是这张图,它显示了几种迭代器之间存在的转换关系:
图中显示了从iterator到const_iterator、从iterator到reverse_iterator和从reverse_iterator到const_reverse_iterator可以进行隐式转换。并且,reverse_iterator可以通过调用其base成员函数转换为iterator。const_reverse_iterator也可以类似地通过base转换成为const_iterator。一个图中无法显示的事实是:通过base得到的也许并非你所期待的iterator。我们将会在条款28中详细讨论这一点。
你应该发现了没有办法从一个const_iterator转换得到一个iterator,也无法从const_reverse_iterator得到reverse_iterator。这一点非常重要,因为这意味着如果你有一个const_iterator或者const_reverse_iterator,你会发现让南让它们和容器的一些成员函数合作。那些成员函数要求iterator,而你无法从const迭代器类型反过来得到iterator。当你需要指出插入位置或删除的元素时,const迭代器几乎没有用。
千万不要傻乎乎的宣称const迭代器一无是处。不,它们可以与算法默契配合,因为算法通常并不关心迭代器是什么类型,只要是适当的种类(category)就可以了,很多容器的成员方法也接受const迭代器。只有insert和erase的一些形式有些吹毛求疵。
我写的是如果你要指出插入的位置或删除的元素时,const迭代器“几乎”没有用。这暗示了并不是完全没用。那是真的。如果你找到了一个方法可以从const_iterator或const_reverse_iterator得到一个iterator,那么它们就有用。那经常是有可能的。但这种方法并不总是行得通,而且就算可行,完成的方法也很不直观,也很缺乏效率。这个主题将足够充实一个它自己的条款,所以如果你对这个细节感兴趣的话,请转到条款27。现在,我们已经有足够的理由相信应该尽量使用iterator取代const或者reverse类型的迭代器:
所有这些东西联合起来就能看出,如果你尽量使用iterator代替const或reverse类型的迭代器,可以使得容器的使用更简单,更高效而且可以避免潜在的bug。
事实上,你可能会更多地面临在iterator与const_iterator之间的选择.iterator与reverse_iterator之间的选择显而易见——依赖于从前到后或从后到前的遍历。你可以选择你需要的一种,而且即使你选择了reverse_iterator,当你要调用需要iterator的容器成员函数时,你仍然可以通过base得到相应的iterator(可能需要一些调整,参见条款28)。
当在iterator和const_iterator之间作选择的时候,你有更充分的理由选择iterator,即使const_iterator同样可行而且即使你并不需要调用容器类的任何成员函数。其中的令人讨厌的原因包括iterator与const_iterator之间的比较。我希望我们都可以赞成这是合理的代码:
typedef deque<int> IntDeque; // typedef可以极大地简化
typedef IntDeque::iterator Iter; // STL容器类和iterator
typedef IntDeque::const_iterator ConstIter; // 的操作。
Iter i;
ConstIter ci;
... // 同一个容器
if (i == ci) ... // 比较iterator和const_iterator
我们所做的只是同一个容器中两个迭代器之间的比较,这是STL中最基本的动作。唯一的变化是等号的一边的类型是iterator,而另一边的类型是const_iterator。这应该不是问题,因为iterator应该在比较之前隐式的转换成const_iterator,真正的比较应该在两个const_iterator之间进行。
对于设计良好的STL实现而言,情况确实如此。但对于其它一些实现,这段代码甚至无法通过编译。原因在于,这些实现将const_iterator的operator==作为const_iterator的一个成员函数而不是非成员函数。而问题的解决之道显得非常有趣:只要像这样交换两个iterator的位置:
if (ci==i)... // 当上面比较无法
// 通过编译时的解决方法
不仅是比较是否相等,只要你在同一个表达式中混用iterator和const_iterator(或者reverse_iterator和const_reverse_iterator),这样的问题就可能会出现。比如,当你试图在两个随机存取迭代器之间进行减法操作时:
if (i-ci >= 3) ... // 如果i与ci之间至少有三个元素...
如果迭代器的类型不同,你的(正确的)代码可能会被(错误地)拒绝。你可以想见解决的方法(交换i和ci的位置),但这次,不只是用i-ci替换ci-i了:
if (ci+3 <= i) ... // 当上面的代码无法
// 通过编译时的解决方法
避免这类问题的最简单的方法是减少混用不同类型的迭代器的机会,换句话说,又回到了尽量用iterator代替const_iterator。从const correctness的角度来看(一个固然有价值的角度),仅仅为了避免一些潜在的STL实现的弊端(而且,这些弊端都有较为直接的解决途径)而抛弃const_iterator显得有欠公允。但综合考虑到iterator与一些容器类成员函数的粘连关系,从实践得出const_iterator没有iterator好用的结论是很难避免的。更何况,有时并不值得卷入const_iterator的麻烦中去。
[1]关于这个主题的完整的文章,请参考1999年2月《Embedded Systems Programming》上刊登的《const T vs. T const》,作者是Dan Saks。
[2]“iterator比较特殊”的原因并不清楚。HP的最早的STL实现包含了带有iterator参数的insert和erase,而这个设计问题在标准化的过程中并没有重新考虑。但是在以后,这可能会改变,因为程序库工作组发布了#180记录,说明了“这个问题在下一个标准版本中会作为综合回顾的const问题而被考虑”(C++程序库问题可以从看到)