条款27:用distance和advance把const_iterator转化成iterator
条款27:用distance和advance把const_iterator转化成iterator
条款26中指出有些容器成员函数只接受iterator作为参数,而不是const_iterator。那么,如果你只有一个const_iterator,而你要在它所指向的容器位置上插入新元素呢?也就是如何把const_iterator转化为iterator呢?因为正如条款26所解释的,并不存在从const_iterator到iterator之间的隐式转换,所以你必须成为这次行动的主角。
我知道你在想什么。你正在想,“每当无路可走的时候,就举起大锤!”。在C++的世界里,你的意思只能是:映射。这种想法很可耻。真不知道你是哪儿学来的。
让我们面对困扰在你面前的问题。看看当你把一个const_iterator映射为iterator时会发生什么:
typedef deque<int> IntDeque; // 方便的typedef typedef IntDeque::iterator Iter; typedef IntDeque::const_iterator ConstIter; ConstIter ci; // ci是const_iterator ... Iter i(ci); // 错误!没有从const_iterator // 到iterator隐式转换的途径 Iter i(const_cast<Iter>(ci)); // 仍是个错误!不能从const_iterator // 映射为iterator!
这里只是以deque为例,但是用其它容器类——list、set、multiset、map、multimap甚至条款25描述的散列表容器[1]——的结果一样。使用映射的行也许在vector或string的代码时能够编译,但这是我们马上要讨论的非常特殊的情形。
包含映射的代码不能通过编译的原因在于,对于这些容器而言,iterator和const_iterator是完全不同的类。它们之间并不比string和complex<float>具有更多的血缘关系。在两个毫无关联的类之间进行const_cast映射是荒谬的,所以reinterpret_cast、static_cast甚至C风格的映射也会导致同样的结果。
唉,不能编译的代码对于vector和string容器来说也许能够通过编译。那是因为通常情况下大多数实现都会采用真实的指针作为那些容器的迭代器。就这种实现而言,vector<T>::iterator是T*的typedef,而vector<T>::const_iterator是const T*的typedef,string::iterator是char*的typedef,而string::const_iterator是const char*的typedef。在这种实现的情况下,用const_cast把const_iterator映射成iterator当然可以编译而且没有问题,因为const_iterator与iterator之间的const_cast映射被最终解释成const T*到T*的映射。但是,即使是在这种实现中,reverse_iterator和const_reverse_iterator也是真正的类,所以你仍然不能直接用const_cast把const_reverse_iterator映射成reverse_iterator。而且,正如条款50解释的,这些实现通常只会在Release模式时才使用指针表示vector和string的迭代器[2]。所有这些事实表明,把const迭代器映射为迭代器是病态的,即使是对vector和string来说也时,因为移植性很值得怀疑。
如果你得到一个const_iterator并且可以访问它所指向的容器,那么有一种安全的、可移植的方法获取它所对应的iterator[3],而且,用不着陷入类型系统的转换。下面是解决思路的本质,虽然在它编译前还要稍作修改:
typedef deque<int> IntDeque; // 和以前一样 typedef IntDeque::iterator Iter; typedef IntDeque::const_iterator ConstIter; IntDeque d; ConstIter ci; ... // 让ci指向d Iter i(d.begin()); // 初始化i为d.begin() advance(i, distance(i, ci)); // 把i移到指向ci位置 // (但请留意下面关于为什么 // 在它编译前要调整的原因)
这种方法看上去非常简单,直截了当,也很让人吃惊吧。要得到与const_iterator指向同一位置的iterator,首先将iterator指向容器的起始位置,然后把它向前移到和const_iterator距离容器起始位置的偏移量一样的位置即可!这个任务得到了两个函数模板advance和distance的帮助,它们都在<iterator>中声明。distance返回两个指向同一个容器的iterator之间的距离;advance则用于将一个iterator移动指定的距离。如果i和ci指向同一个容器,那么表达式advance(i, distance(i, ci))会将i移动到与ci相同的位置上。
如果这段代码能够通过编译,它就能完成这种转换任务。但似乎事情并不那么顺利。想知道为什么,先来看看distance的定义:
template<typename InputIterator> typename iterator_traits<InputIterator>::difference_type distance(InputIterator first, InputIterator last);
不要被这个函数的长达56个字符的返回类型卡住,也不用理会difference_type是什么东西。取而代之的是,把注意力集中在参数的类型InputIterator:
template<typename InputIterator> typename iterator_traits<InputIterator>::difference_type distance(InputIterator first, InputIterator last);
当遇到distance调用时,你的编译器需要根据使用的实参类型推断出InputIterator的类型。再来看看我所说的不太正确的distance调用:
advance(i, distance(i, ci)); // 调整i,指向ci位置
有两个参数传递给distance,i和ci。i的类型是Iter,即deque<int>::iterator的typedef。对编译器来说,这表明调用distance的InputIterator是deque<int>::iterator。但ci是ConstIter,即deque<int>::const_iterator的typedef。这表明那个InputIterator是deque<int>::const_iterator。InputIterator不可能同时有两种不同的类型,所以调用distance失败。一般会造成一些冗长的出错信息,可能会也可能不会说明是编译器无法得出InputIterator是什么类型。
要顺利地调用distance,你需要排除歧义。最简单的办法就是显式的指明distance调用的模板参数类型,从而避免编译器自己得出它们的类型:
advance(i, distance<ConstIter>(i, ci));
我们现在知道了怎么通过advance和distance获取const_iterator相应的iterator了。但另一个我们现在一直避开却很值的考虑的实际问题是:这个技巧的效率如何?答案很简单。取决于你所转换的究竟是什么样的迭代器。对于随机访问的迭代器(比如vector、string和deque的)而言,这是常数时间的操作。对于双向迭代器(也就是,所有其它容器和包括散列容器的一些实现[4](参见条款25))而言,这是线性时间的操作。
因为它可能花费线性时间的代价来产生一个和const_iterator等价的iterator,并且因为如果不能访问const_iterator所属的容器这个操作就无法完成。从这个角度出发,也许你需要重新审视你从const_iterator产生iterator的设计。事实上那样的考虑帮助激发了条款26,它建议你当处理容器时尽量用iterator代替const和reverse迭代器。
[1] 两个最常见的基于散列表的STL容器实现来自于Dinkumware和SGI。你可以从P.J.Plauger 1998年11月份的CUJ专栏《Hash Tables》中找到一个Dinkumware方法的概览。我所知道的唯一的SGI的实现方法的概览来自Effective STL的条款25,但它的接口的描述在SGI的STL网站。
[2] 当使用STLport的调试模式时会出现这种情况。你可以从STLport的网站上了解到STLport和它的调试模式。
[3] 我后来发现在这里描述的这种方法可能在使用引用计数的string实现上失败。细节请参考在http://www.aristeia.com/BookErrata/estl1e-errata.html的jep的 8/22/01关于本书第121页的注释。
[4] Dinkumware的散列容器提供了双向的迭代器。SGI的、STLport的和Metrowerks的散列容器只提供了前向迭代器。