Item 13 : 优先考虑 const_iterator 而非 iterator

优质
小牛编辑
117浏览
2023-12-01

条款 13:优先考虑const_iterator而非iterator

STL const_iterator等价于指向常量的指针。它们都指向不能被修改的值。标准实践是能加上const就加上,这也指示我们对待const_iterator应该如出一辙。

上面的说法对C++11和C++98都是正确的,但是在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;     // typetypedef
std::vector<int>::const_iterator ConstIterT; // defs
std::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); // 可能无法通过编译,原因见下

typedef不是强制的,但是可以让类型转换更好写。(你可能想知道为什么我使用typedef而不是Item 9提到的别名声明,因为这段代码在演示C++98做法,别名声明是C++11加入的特性)

之所以std::find的调用会出现类型转换是因为在C++98中values是非常量容器,没办法简简单单的从非常量容器中获取const_iterator。严格来说类型转换不是必须的,因为用其他方法获取const_iterator也是可以的(比如你可以把values绑定到常量引用上,然后再用这个变量代替values),但不管怎么说,从非常量容器中获取const_iterator的做法都有点别扭。

当你费劲地获得了const_iterator,事情可能会变得更糟,因为C++98中,插入操作的位置只能由iterator指定,const_iterator是不被接受的。这也是我在上面的代码中,将const_iterator转换为iterat的原因,因为向insert传入const_iterator不能通过编译。

老实说,上面的代码也可能无法编译,因为没有一个可移植的从const_iteratoriterator的方法,即使使用static_cast也不行。甚至传说中的牛刀reinterpret_cast也杀不了这条鸡。(它C++98的限制,也不是C++11的限制,只是const_iterator就是不能转换为iterator,不管看起来对它们施以转换是有多么合理。)不过有办法生成一个iterator,使其指向和const_iterator指向相同,但是看起来不明显,也没有广泛应用,在这本书也不值得讨论。除此之外,我希望目前我陈述的观点是清晰的:const_iterator在C++98中会有很多问题。这一天结束时,开发者们不再相信能加const就加它的教条,而是只在实用的地方加它,C++98的const_iterator不是那么实用。

所有的这些都在C++11中改变了,现在const_iterator即容易获取又容易使用。容器的成员函数cbegincend产出const_iterator,甚至对于非常量容器,那些之前只使用iterator指示位置的STL成员函数也可以使用const_iterator了。使用C++11 const_iterator重写C++98使用iterator的代码也稀松平常:

std::vector<int> values; // 和之前一样
…
auto it =                                             // 使用cbegin
    std::find(values.cbegin(),values.cend(), 1983); // 和cend
values.insert(it, 1998);

现在使用const_iterator的代码就很实用了!

唯一一个C++11对于const_iterator支持不足(译注:C++14支持但是C++11的时候还没)的情况是:当你想写最大程度通用的库,并且这些库代码为一些容器和类似容器的数据结构提供非成员函数beginend(以及cbegincendrbeginrend)而不是成员函数(其中一种情况就是原生数组)。最大程度通用的库会考虑使用非成员函数而不是假设成员函数版本存在。

举个例子,我们可以泛化下面的findAndInsert

template<typename C, typename V>
void findAndInsert(C& container,     // 在容器中查找第一次
const V& targetVal,                 // 出现targetVal的位置,
const V& insertVal)                 // 然后插入insertVal
{                                     
    using std::cbegin; // there
    using std::cend;
    auto it = std::find(cbegin(container),     // 非成员函数cbegin
                        cend(container),     // 非成员函数cend
                        targetVal);
    container.insert(it, insertVal);
}

它可以在C++14工作良好,但是很遗憾,C++11不在良好之列。由于标准化的疏漏,C++11只添加了非成员函数beginend,但是没有添加cbegincendrbeginrendcrbegincrend。C++14修订了这个疏漏,如果你使用C++11,并且想写一个最大程度通用的代码,而你使用的STL没有提供缺失的非成员函数cbegin和它的朋友们,你可以简单的抛出你自己的实现。比如,下面就是非成员函数cbegin的实现:

template <class C>
auto cbegin(const C& container)->decltype(std::begin(container))
{
    return std::begin(container); // 解释见下
}

你可能很惊讶非成员函数cbegin没有调用成员函数cbegin吧?但是请跟逻辑走。这个cbegin模板接受任何容器或者类似容器的数据结构C,并且通过const引用访问第一个实参container。如果C是一个普通的容器类型(如std::vector<int>),container将会引用一个常量版本的容器(即const std::vector<int>&)。对const容器调用非成员函数begin(由C++11提供)将产出const_iterator,这个迭代器也是模板要返回的。用这种方法实现的好处是就算容器只提供begin不提供cbegin也没问题。那么现在你可以将这个非成员函数cbegin施于只支持begin的容器。

如果C是原生数组,这个模板也能工作。这时,container成为一个const数组。C++11为数组提供特化版本的非成员函数begin,它返回指向数组第一个元素的指针。一个const数组的元素也是const,所以对于const数组,非成员函数begin返回指向const的指针。在数组的上下文中,所谓指向const的指针,也就是const_iterator了。

回到最开始,本条款的中心是鼓励你只要能就使用const_iterator。最原始的动机是——只要它有意义就加上const——C++98就有的思想。但是在C++98,它(译注:const_iterator)只是一般有用,到了C++11,它就是极其有用了,C++14在其基础上做了些修补工作。

记住

  • 优先考虑const_iterator而非iterator
  • 在最大程度通用的代码中,优先考虑非成员函数版本的beginendrbegin等,而非同名成员函数