Effective Modern C++ 条款13:优先考虑const_iterator而非iterator
作为C++开发者,我们经常听到"尽可能使用const"的建议。这一原则同样适用于STL迭代器的选择。本文将深入探讨为什么const_iterator
应该是你的首选,以及如何在不同C++标准中有效地使用它们。
const_iterator的本质
const_iterator
是STL中指向常量元素的迭代器,概念上等同于指向常量的指针(pointer-to-const)。就像我们喜欢用const
修饰不应该被修改的变量一样,只要不需要修改迭代器指向的元素,使用const_iterator
就是更安全、更符合设计意图的选择。
C++98时代的困境
在C++98标准中,虽然const_iterator
已经存在,但使用它们却充满挑战:
- 创建困难
从non-const容器获取const_iterator
需要繁琐的类型转换:
std::vector<int> values;
typedef std::vector<int>::const_iterator ConstIterT;ConstIterT ci = std::find(static_cast<ConstIterT>(values.begin()),static_cast<ConstIterT>(values.end()),1983);
- 功能受限
许多STL操作如insert()
和erase()
在C++98中只接受iterator
,不接受const_iterator
。这意味着即使你获得了const_iterator
,也可能需要再转换回iterator
——这一转换在标准中甚至没有明确定义。
- 性能考量
由于这些限制,许多开发者放弃了使用const_iterator
,即使在不需要修改元素的场景下也使用普通的iterator
,这降低了代码的表达性和安全性。
C++11的革命性改进
C++11标准极大地改善了const_iterator
的实用性:
- 新增便捷方法
引入了cbegin()
和cend()
成员函数,即使对non-const容器也能直接获取const_iterator
:
auto it = std::find(values.cbegin(), values.cend(), 1983);
- 操作支持扩展
STL操作如insert()
和erase()
现在都接受const_iterator
作为参数,消除了C++98中的主要使用障碍。
- 语法简化
结合auto
类型推导,使用const_iterator
变得异常简洁:
std::vector<int> values;
auto it = std::find(values.cbegin(), values.cend(), 1983);
values.insert(it, 1998);
通用代码的最佳实践
在编写模板代码时,我们需要考虑更广泛的容器类型,包括原生数组和第三方容器。这里有一些重要建议:
- 优先使用非成员函数版本
C++14提供了完整的非成员函数cbegin/cend
等,应该优先使用它们而非成员函数版本:
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++11的兼容方案
如果你的环境限制在C++11,可以自行实现缺失的非成员cbegin
:
template <class C>
auto cbegin(const C& container)->decltype(std::begin(container))
{return std::begin(container);
}
这个实现巧妙之处在于:
- 对标准容器,
begin()
对const对象返回const_iterator
- 对原生数组,返回指向const的指针
- 对只提供
begin()
的第三方容器也能工作
实际开发中的应用场景
- 只读遍历
任何不需要修改容器内容的遍历都应该使用const_iterator
:
for (auto it = values.cbegin(); it != values.cend(); ++it) {process(*it); // 假设process不需要修改元素
}
- 算法应用
大多数STL算法如find
、count
、accumulate
等都不修改元素,应该配合const_iterator
使用:
auto pos = std::find_if(values.cbegin(), values.cend(), [](int val) { return val > 0; });
- 多线程环境
在多线程代码中,const_iterator
能更明确地表达只读意图,有助于避免数据竞争。
性能考量
有些人担心使用const_iterator
会影响性能,但实际上:
- 在release构建中,好的编译器会为
iterator
和const_iterator
生成相同的机器码 - 使用
const_iterator
带来的编译期检查可以避免潜在的错误,减少调试时间 - 更明确的语义有助于编译器进行更好的优化
现代C++的进一步支持
C++17和C++20继续强化了const正确性的支持:
std::as_const
可以方便地获取const视图- 范围for循环的const版本更简洁
- 概念(concepts)可以更好地约束模板参数
总结建议
- 默认选择:在不需要修改元素的场景下,优先使用
const_iterator
- 现代标准:充分利用C++11及以后版本的
cbegin()/cend()
等便利方法 - 通用代码:在模板中优先使用非成员函数版本的begin/end系列
- 代码审查:将"不必要的非const迭代器使用"加入代码审查检查项
记住,好的C++代码不仅追求功能正确,还追求表达准确。const_iterator
就是帮助我们实现这一目标的重要工具之一。正如Scott Meyers在《Effective Modern C++》中所说:“const是伪装的文档,也是伪装的编译器可验证的正确性约束。”