13.以const_iterator代替iterator

前言

 
(建议将本节与Effective STL Item26、27结合阅读)
理论上来说,当你不需要改变迭代器所指对象时,你应当尽量使用const_iterator,这条规则对于C++98与C++11都适用。但实际上在C++98中,使用const_iterator会面临处处受限的窘境。


问题实例

 
举例而言,假若你希望在一个vector<int>中搜索1983第一次出现的位置,并在其位置插入一个1998,如果当前vector内不存在1983,则在vector尾端插入。使用C++98中的iterator可以轻松完成上述操作:

1
2
3
4
std::vector<int> values;

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

由于我们并没有改变iterator指向的元素,理论上我们应当将其改为const_iterator。话虽如此,但在C++98中却并不简单,下面展示了一个似是而非的修正版本:
1
2
3
4
5
6
7
8
typedef std::vector<int>::iterator IterT; 
typedef std::vector<int>::const_iterator ConstIterT;
std::vector<int> values;

ConstIterT ci = std::find(static_cast<ConstIterT>(values.begin()),
static_cast<ConstIterT>(values.end()),
1983);
values.insert(static_cast<IterT>(ci), 1998); // may not compile

find函数中使用了强制转换,原因在于:在C++98中,从一个non-const容器中获取const_iterator并非易事。当然了,也不一定非要使用强制转换,你可以定义一个常量引用指向values,然后在源代码中以该常量引用替换values。但不管怎么说,从获取一个指向non-const容器内部元素的const_iterator总是很麻烦。

即使你真的获取到了const_iterator,事情也未必会发展地如你所愿。在C++98中,插入和删除函数参数必须为iterator类型,这也就是上述代码最后千方百计将const_iterator转换回来的原因。不过,在上述代码的注释中标明强制转换可能无法编译(正确的转换方法与为何会无法编译可见Effective STL Item26、27)。


C++11中的const_iterator

 
C++11中一切都发生了改变,通过cbegin与cend成员函数,non-const容器也可以轻松获取const_iterator。此外,以往STL中需要以iterator标明位置的成员函数(例如insert、erase等等)现如今也可以用const_iterator作为参数,现在我们用C++11再次完成刚才所说的功能:

1
2
3
4
std::vector<int> values; // as before

auto it = std::find(values.cbegin(),values.cend(), 1983);
values.insert(it, 1998);


C++14中的const_iterator

 
尽管C++11中的const_iterator已经大幅度提高了我们的生产力,但其对于开发通用库程序时还是略显不足。这些程序可能会接收一个容器,返回其数据结构起始和结束的位置。我们认为真正的泛型应当以非成员函数的形式出现,而非成员函数。

假设我们需要泛型之前的findAndInsert程序:

1
2
3
4
5
6
7
template<typename C, typename V>
void findAndInsert(C& container, const V& targetVal,const V& insertVal){
using std::cbegin;
using std::cend;
auto it = std::find(cbegin(container),cend(container),targetVal);
container.insert(it, insertVal);
}

遗憾的是上述程序只能在C++14中生效,原因在于C++11只添加了begin、end这两种非成员函数泛型版本,C++14才完成了cbegin、cend的加入。

但我们也可以稍加变动,在C++11中实现自己的cbegin:

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

其工作原理为:如果实参为non-const,那么container将会成为一个const C&,并且对container调用begin将得到一个const_iterator。这种实现的一大优点在于对于那些带有begin成员函数却不带有cbegin成员函数的容器,你甚至可以直接在函数内部调用begin转换之。

如果C是一个内置数组,那么这个模板函数也能够正确地运行,因为container被推衍为一个const数组的引用,C++11的非成员函数begin会返回一个指向数组首元素的指针,又由于数组现为const,因此指针类型为一个指向const的指针,这与const_iterator是等价的。


总结

  1. 以const_iterator代替iterator。
  2. begin,end之类函数的非成员函数版本比其成员函数版本更加适合于泛型程序。