Effective Modern C++: Item 13 -> 优先选择const_iterators而不是iterators

来源:互联网 发布:hp p1606dn 网络打印 编辑:程序博客网 时间:2024/05/22 01:59

优先选择const_iterators而不是iterators

const_iterators是STL里面对指向常量的指针的等价物。它们指向一些不能被修改的值。只要可以就使用const的标准做法,让我们也应该在任何需要一个iterator并且不需要修改iterator指向的东西的时候去使用const_iterator。

这对于C++98和C++11都适用,但是在C++98里,对const_iterator的支持却很无力。创建它们并不容易,并且一旦你创建了他们,你可以使用它的方式也很受限。例如,假设你想在一个std::vector<int>里面搜索1983(这是”C++”取代”C with Classes”作为编程语言名字的一年)第一次出现的地方,然后在那个位置插入值1998(这是第一份ISO C++标准被采用的一年)。如果vector里面没有1983,那么应该在vector的最后插入该值。在C++98里面使用iterator,很简单:

std::vector<int> values;…std::vector<int>::iterator it =    std::find(values.begin(),values.end(), 1983);values.insert(it, 1998);

但是iterator在这并不是合适的选择,因为这份代码从来没有修改过iterator指向的东西。修改这份代码去使用const_iterator应该不算啥事,但是在C++98里,这就是事了。下面是一种概念上的方法,尽管可能并不正确:

typedef std::vector<int>::iterator IterT; // typetypedefstd::vector<int>::const_iterator ConstIterT; // defsstd::vector<int> values;…ConstIterT ci =    std::find(static_cast<ConstIterT>(values.begin()), // cast              static_cast<ConstIterT>(values.end()), // cast              1983);values.insert(static_cast<IterT>(ci), 1998); // may not                                             // compile; see                                             // below

当然typedef并不是必须的,但是它们让里面的cast代码更好写。(如果你在想为什么我要用typedef而不是按照Item 9里面的建议去使用别名声明的话,那是因为这个例子展示的是C++98代码,而别名声明是C++11才有的新特性)

std::find调用需要使用cast是因为values是一个non-const的容器,并且在C++98里,没有一种简单方式可以从一个non-const容器里取出const_iterator。cast并不是严格必须的,因为还可以通过其他方式来获得const_iterator(比如,你可以将values绑定到一个指向const的引用变量上,然后在你的代码中values出现的地方使用它),但是哪一种方式,从一个non-const容器里面获得const_iterator元素都需要一定量的控制。

一旦你有了const_iterator,情况经常会变得更糟,因为在C++98里,插入(或者擦除)的位置只能由iterator指定,const_iterator是不被接受的。这也就是为什么,在上面的代码中,我把const_iterator(我小心翼翼地从std::find里面获取的)又cast成iterator:传递一个const_iterator给insert是不能通过编译的。

老实说,我上面展示的代码可能也编译不通过,因为并不存在一种从const_iterator到iterator的转换,即使是static_cast也不行。就算是语义大锤—reinterpret_cast—也办不到。(这不仅仅是C++98的限制,对于C++11也一样。const_iterator就是不能转换成iterator,不管看起来它们多么应该能转换)
有一些可行的方式可以产生指向和const_iterator一样的iterator,但是它们并不显而易见,并不通用,也不值得在本书中讨论。除此之外,我希望到现在为止我表达的很清楚了:const_iterator在C++98中是一个大麻烦,它们几乎不值得去使用。所以最终,程序员只要可能就不会去使用const_iterator,而只有在必须的时候才去使用。在C++98中,const_iterator就是没那么实用。

而在C++11中,一切都变了!现在const_iterartor既方便获得又方便使用。容器的成员函数cbegin和cend产生const_iterator,即使该容器是non-const的,并且STL中使用iterator来指定位置的成员函数(比如insert和erase)实际使用的也是const_iterator。将C++98中使用iterator的代码修改成C++11中使用const_iterator的代码非常简单:

std::vector<int> values;    //as before...auto it =                   //use cbegin    std::find(values.cbegin(),values.cend(),1983);  //and cendvalues.insert(it,1998);

这就是实用的使用了const_iterator的代码!

关于C++11中对于const_iterator的支持稍显不足的唯一情况就是当你想写最大程度通用的库函数。这类代码将一些提供了begin和end(还有cbegin,cend,rbegin等)作为非成员函数的容器以及类似容器的数据结构也考虑进去了。比如,这就是内置数组的case,也同时是一些具有某种接口的第三方库的case。最大程度通用的代码因此使用非成员函数,而不是假设存在成员函数。

例如,我们将我们之前研究的代码泛化成一个findAndInsert模板:

template<typename C, typename V>void findAndInsert(C& container,            //in container,find                    const V& targetVal,     //first occurrence                    const V& inserVal)      //of targetVal, then{                                           //insert insertVal there    using std::cbegin;    using std::cend;    auto it = std::find(cbegin(container),  //non-member cbegin                        cend(container),    //non-member cend                        targetVal);    container.insert(it,insertVal);}

这在C++14中正常工作,但是不幸的是,C++11不行。标准化期间经过勘漏,C++增加了非成员函数begin和end,但是没有增加cbegin,cend,rbegin,rend,crbegin和crend。C++14修正了这个。

如果你正在用C++11,你想写一个最大程度通用的代码,你所使用的库没有一个提供了非成员函数cbegin和其他友元函数的模板,你可以很容易写出自己的实现版本。例如,下面是一个非成员函数cbegin的实现:

template <class C>auto cbegin(const C& container) ->decltype(std::begin(container)){    return std::begin(container);   //see explanation below}

你很吃惊:非成员函数cbegin居然没有调用成员函数cbegin,对不对?我也是。但是沿着它的逻辑看。这个cbegin模板接收任意表示类似容器的数据结构C,并且通过这个指向const的引用参数,container,来访问这个值。如果C是一个传统的容器类型(比如说std::vector<int>),container将是那个容器的const版本的一个引用(比如,一个const std::vector<int>&)。对一个const容器调用非成员函数begin(C++11提供的)将得到一个const_iterator,并且这个iterator就是这个模板要返回的。通过这种方式实现的优势在于即使对于那些提供了begin成员函数(对于容器,就是C++11的非成员函数begin)但没有提供cbegin成员函数的容器也适用。你也可以因此将这个cbegin应用在只支持begin的容器上了。

如果C是一个内置数组类型,该模板也可以起作用。在那种情况下,container变成了一个const数组的引用。C++给数组提供了一个特化版本的非成员函数begin,其返回一个指向数组第一个元素的指针。一个const数组的元素是const类型,所以对于const数组,非成员函数begin返回的指针是一个指向const的指针,而一个指向const的指针实际上就是一个数组的const_iterator。(想知道模板如何对内置数组类型进行特化,参考Item 1关于接受数组引用参数的模板类型推断方面的讨论)

但是回归根本,这一条款的目的在于鼓励你只要可以就使用const_iterator。最根本的动机—只要可以就使用const—其实在C++11之前就存在了,但是在C++98里,当涉及到iterator时就不太实用了。在C++11里,这个极其实用,而C++14则对C++11遗留的一些问题做了修复。

要点记忆

  • 有限选择const_iterator而不是iterator
  • 在最大程度通用的代码里,优先选择非成员版本的begin,end,rbegin等而不是成员函数的版本
阅读全文
0 0