Effective Modern C++ 条款13 比起iterator更偏爱const_iterator

本文探讨了在C++98与C++11中使用const_iterator的优点及其实现方式的变化。C++11中const_iterator的使用更加便捷,支持更好的泛型编程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

比起iterator更偏爱const_iterator

STL中的const_iterator等效于常量指针(pointers-to-const),它们指向的值是不能被修改的。标准实践建议我们尽可能地使用const,同样,如果迭代器指向的对象不需要修改,那么我们应该使用const_iterator

上面的说法在C++98和C++11中都是正确的,但是在C++98中,const_iterator只有部分支持,创建它们不容易,而且使用它也受限制。例如,你想要在std::vector<int>中查找第一个1983的位置(那一年“C with Classes”编程语言更名为“C++”),然后在那个位置插入数值1998(那一年C++第一个ISO标准被采用)。如果容器中不存在1983这个值,就在容器的末尾插入。使用C++98的迭代器,这很容易:

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会出现点问题。这里有个方法在概念上是可靠的,不过仍然不正确:

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()),  // cast
               static_cast<ConstIterT>(values.end()),    // cast
               1983);
                                      `
values.insert(static_cast<IterT>(ci), 1998); // 可能编译不通过,详情看下面

当然,typedef不是必需的,不过它可以让我们写类型转换时容易点(你可能会问为什么不遵循条款9的别名声明,原因是这个例子展示的是C++98的代码,而别名声明是C++11的新特性)。

在函数std::find中出现了类型转换,这是因为values不是non-const容器,并且在C++98中从non-const容器中获取const_iterator没有简单的办法。在严格意义上,类型转换不是必需的,因为你可以从另外的途径获取const_iterator(例如,把values绑在一个常量引用(reference-to-const)的变量上,然后在代码中用这个变量代替values),但是无论你使用哪种方法,从一个non-const容器获取指向元素的const_iterator总会出现点意外。

如果你拿到了const_iterator,问题更多,因为C++98容器的插入(和删除)位置都是用iterator表示,const_iterator是不被接受的,这就为什么在上面的代码中我把const_iterator转换为iterator:把const_iterator传递给insert是不能通过编译的。

实话说,我给出的代码还是无法通过编译,因为就算是用static_cast,关于const_iterator转换到iterator也没有轻便的惯例(意思就是不行?)就算是神器reinterpret_cast也不行哦(这不只是C++98的限制,在C++11中也不能简单的把const_iterator转换成iterator)。现在,我觉得我的观点已经很清晰了:在C++98中const_iterator有那么多问题,真的是不值得用。最后,开发者不应尽可能的使用const,而是在它实用(practical)时使用,然后在C++98中,const_iterator一点都不实用。


在C++11中一切都改变了。现在const_iterator即容易获取又容易使用。就算是non-const容器,容器的成员函数cbegincend返回const_iterator,然后STL那些用iterator表示位置的成员函数(例如insert和earse)实际上都使用了const_iterator。把C++98旧代码中iterator修改为const_iterator在C++11中是再平常不过了:

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

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


C++11提供的获取const_iterator的方法在你想要编写最大限度通用库代码时略显不足。这些代码为容器或者类似容器的数据结构提供非成员函数的beginend(加上cbegin,cend,rbegin等)代码。例如,就像内置的数组,它就是使用第三方库提供的自由函数的接口。因此通用库代码就是使用非成员函数来实现已知的成员函数的功能。

例如,我们有个findAndInsert模板:

template <typename C, typename V>
void findAndInsert(C& container,        // 在container中,
                   const V& targetVal,   // 找到targetVal的第一次出现位置,
                   const V& insertVal)  // 然后把insertVal插入那个位置
{
    using std::cbegin;
    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,但想写出上面效果的代码,却没有库来提供一个非成员的cbegin模板,那么你可以自己写一个。例如,这里有一个非成员cbegin的实现:

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

你是不是很惊讶这个非成员cbegin没有调用成员cbegin 呢?其实我也是。不过这是有逻辑的。cbegin模板接受所有行为像容器的参数C,然后用常量引用(reference-to-const)获得这个参数container,如果C是传统的容器类型(例如std::vector<int>),container将会常量引用那个容器C(类型变成const std::vector<int>&)。一个常量容器使用非成员函数begin(C++11提供)会返回一个const_iterator,这个类型就是模板返回的类型。你可以把这个由begin支持的cbegin直接用于容器。

这个模板也可以用于内置数组。这样的话,container的就是一个常量数组引用。C++11为数组提供了一个特别的begin,它返回指针数组首元素的指针。常量数组的元素是常量,所以begin返回的首元素指针是常量指针,常量指针,在实际上,也就是数组的常量迭代器。(数组的模板推断请看条款1).


现在回到重点,这条款的观点是鼓励你尽量使用const_iterator。基本动机是——当const有意义时就使用——在C++11,而不是不实用的C++98。在C++11,它非常实用,C++14还解决了C++11留下的未完成的工作。

总结

需要记住的2点:

  • 比起iterator更偏爱const_iterator
  • 在最大限度通用代码中,比起成员函数,更偏向使用非成员版本的beginendrbegin等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值