
一、迭代器
在标准库中,STL通常被划分为几个主要模块:容器、算法、分配器、适配器、仿函数(函数对象)以及迭代器。作为算法与容器之间的桥梁,迭代器提供了一种统一的方式来遍历和访问容器中的元素,而无需关心容器内部的实现细节。通常,迭代器可以分为五类:输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器。当然,在STL中,还通过适配器扩展出了反向迭代器、插入迭代器和流迭代器等,它们都基于前述的五种基础迭代器构建。
二、迭代器面临的特殊应用
一般情况下,上述五类迭代器加上扩展的适配器迭代器,足以覆盖STL中的绝大多数应用场景。但在实际开发中,我们偶尔会遇到一些特殊情况。请看下面的示例代码:
//代码来自提案
struct A {
//容器的起始和终止迭代器
virtual vector<int>::const_iterator begin();
virtual vector<int>::const_iterator end();
. . .
};
struct B : public A {
virtual vector<int>::const_iterator begin();
virtual vector<int>::const_iterator end();
vector<int> v;
};
const A& ar = get_an_A(...);
for (int x : ar)
do_something(x);
do_something_else(ar);
在这个例子中,基类A提供了操作vector的迭代器接口(begin和end),但它本身并不真正包含一个容器。它的子类B在继承后,可以拥有实际的容器(成员变量v),从而能实现这些迭代器接口。这就带来一个问题:基类A可能需要存储一对代表vector范围的迭代器(即begin和end),但这个被存储的范围迭代器本身可能是空的,这意味着它并未与任何实际的vector关联。这听起来是不是有点抽象?可以类比一下数字0的意义,它们有相似之处。
除了上述场景,在C++的某些字符串处理逻辑中也可能出现类似需求。有需求就会有改进,因此在C++14中,提出了“奇异迭代器”这个概念。
三、奇异迭代器
奇异迭代器,也被称为singular iterators、越界迭代器(past-the-end iterators)或空前向迭代器(Null forward iterators)。它正是为了解决上述场景而提出的一种机制。由于这种迭代器不持有指向任何容器元素的引用,因此它不能被解引用,也不应进行自增或自减操作,否则会导致未定义行为。这一点与C++中的空指针类似——你可以拥有它,但不能用它进行实际的内存访问操作。
奇异迭代器是前向迭代器的一种特殊“空”状态,其内部通常不持有任何有效的指针或引用。它一般通过默认构造函数来构造。根据标准文档的描述,一个奇异迭代器的值可以被赋值为一个非奇异值,或者另一个满足默认构造要求的迭代器值。此时,奇异值会被新值覆盖(其处理逻辑统一到了类似nullptr的赋值语义上)。
奇异迭代器具有以下几个关键特点:
- 不能被解引用。
- 无法进行自增或自减操作。
- 可以进行比较。
- 无字面值,即它受迭代器的类型系统约束。
- 可以自定义实现。
获取奇异迭代器有多种方法,最直接的就是对迭代器类型进行默认初始化,或者自定义一个返回空状态的工厂函数。此外,获取一个空容器的begin()和end()接口返回值,或者直接获取任何容器的end()返回值,得到的也是奇异迭代器。
基于以上分析,如果我们想自定义实现一个奇异迭代器,通常需要实现以下几个部分:
- 构造函数:提供一个默认构造函数,用于创建空的迭代器对象实例。
- 比较操作符:实现
operator== 和 operator!=,通常默认的空值彼此相等。
- 递增操作符:实现前向(和后向,如果是双向迭代器)的
operator++,内部通常为空操作或抛出异常,以避免未定义行为。
- 解引用操作符:实现
operator*,内部应抛出异常(如 std::logic_error)来明确阻止未定义行为。
当然,为了更高的安全性,你也可以实现其他必要的成员函数,这正是“自定义”的灵活性所在。在使用奇异迭代器时务必小心,切勿将其当作正常的迭代器来操作,就像不能操作空指针一样。特别是在与正常迭代器进行比较、赋值等操作时,需要格外注意,否则极易引发未定义行为。关于如何防范此类风险,C++提供了多种策略,例如利用RAII思想或类似智能指针的封装技术,开发者需要根据具体应用场景来抉择。深入了解这些内存和资源管理的最佳实践,可以在云栈社区的C/C++板块找到更多资料。
四、应用场景
那么在C++中,奇异迭代器通常用于哪些场景呢?主要包括以下两种:
-
表示end状态
这与前面提到的通过end()接口获取迭代器是一致的,也完全符合其“越界迭代器”的名字含义。
-
异常等特殊场景下的返回值
在某些特定处理逻辑中,函数需要返回一个迭代器,但当内部出现错误或问题时,可以返回一个奇异迭代器作为安全的占位符。默认构造函数构造的迭代器也常用于此目的。
五、应用和例程
来看一个具体的示例程序:
#include<algorithm>
#include<iostream>
#include<vector>
int main(){
std::vector<int> vec = {3};
// Value initialized
std::vector<int>::iterator it1{};
std::vector<int>::iterator it2{};
if (it1 == it2) {
std::cout << "null forward iterators,it1 equal it2 and ni2 !" << std::endl;
}
// with valid iterator compare
if (it1 == vec.begin()) {
std::cout << " it is undefined behavior!" << std::endl;
}
// with end() comapre
auto it = std::find(vec.begin(), vec.end(), 1); // return end()
if (it == vec.end()) {
std::cout << "end () equal it!" << std::endl;
}
}
这段代码比较简单:首先,它通过值初始化创建了两个奇异迭代器it1和it2,并演示了它们之间可以安全比较(相等)。接着,它尝试将奇异迭代器it1与一个有效迭代器vec.begin()进行比较,注释指出这是未定义行为。最后,它使用std::find算法,当查找失败时返回vec.end()(这也是一个奇异迭代器),并演示了如何安全地检查这种“未找到”的状态。这正是奇异迭代器在算法中作为标志位的典型应用。
六、总结
“空”是一个既普通又至关重要的概念,正如数字世界中的0,它既是起点,也常代表终点,这与中国传统易学中的思想也有暗合之处。所以,当我们初次接触“奇异迭代器”时,不必觉得它有多么新奇或神秘。它实质上是面对特定设计模式时,“空”概念在迭代器领域的一种具体应用体现,可以说是“新瓶装旧酒”,但确实为解决一些边界问题提供了优雅的方案。