复制迭代器是否不好的想法?

9
我在考虑编写一些代码,它将简化为以下内容。 T 是一个集合类型(目前是 std::map,如果有影响的话)。
T coll;

// ...

T::iterator it, prev;

prev = coll.end();

for(it = coll.begin(); it != coll.end(); ++it) {
    if(prev != coll.end())
        { do something involving previous element; }

    do something involving this element;

    prev = it;
}

我的问题是,像这样将 it 复制到 prev 中是否是一个不好的想法?这是一种不好的风格吗?可能会惹恼某些挑剔的人吗?取决于类型 T 的微妙细节,可能会出现问题吗?
我不希望在此循环运行时销毁 coll,也不希望添加或删除任何元素。
一方面,我知道的关于迭代器的所有内容都应该是完全安全的。但另一方面,有些关于迭代器的东西我不知道,这感觉稍微有点可疑,所以我想问问。
补充说明:
另一个原因是我的实际代码不会涉及我上面写的那个 for 循环。我实际上有事件驱动的代码;它看起来更像是这样:
void onStartDoingSomething() {
    it = coll.start();
    prev = coll.end();
    startOperation(it, prev);
    timerStart();
}

void onTimer() {
    if(finished with operation on it) {
        prev = it;
        ++it;
        startOperation(it, prev);
        if(it == coll.end() {
            timerStop();
            call operation finished;
        }
    }
}

void startOperation(T::iterator next, T::iterator prev) {
    if(prev != coll.end()) {
        finish operation on prev;
    }

    if(next != coll.end()) {
        start operation on next;
    }
}

因此,换个方式问我的问题可能是:“你必须在传统的for循环中使用迭代器和集合类的begin()和end()方法吗?还是可以任意使用它们?for循环中是否保留了任何状态,或者所有状态都在迭代器中?”我知道没有像“存储在for循环中的状态”这样的东西,据我所知,对于迭代器来说,包含所有所需状态是至关重要的。因此,如果事实上迭代器不仅包含了所有需要的状态,而且也是100%安全可复制的,那么对于我最初的问题,答案就是“不,这不是一个坏主意”。但是,潜在的可能性是迭代器可能不是100%安全可复制的——例如,如果复制迭代器,增加原始值,然后尝试使用副本时出现了微妙的别名问题,那么这段代码对我来说就感觉有点不靠谱。

看起来对我来说没问题。 - HolyBlackCat
2
你也在这里复制了一个迭代器 it = coll.begin() - eerorika
如果容器不会被修改,更好的方法是使用 std::adjacent_find 和一个谓词,该谓词在容器中每个相邻的值对上调用。只需确保谓词始终返回 false 即可。由于 std::adjacent_find 基本上做了你正在做的事情,即迭代,如果它对于 std::adjacent_find 足够好,那么对于你也足够好。 - Sam Varshavchik
std::map派生?子类化/继承标准容器? - Andriy Tylychko
@AndriyTylychko ISWYM。感谢您的关心,我确实说过“派生自”,但我真正想表达的是“typedef”的同义词,即“typedef for”。所以这里没有问题。 - Steve Summit
显示剩余2条评论
2个回答

11
这取决于迭代器的类型,但对于std::map::iterator来说是可以的。迭代器分为不同的类别,具体取决于它们支持的操作以及它们提供关于所引用值的保证。std::map::iterator符合BidirectionalIterator概念的要求。这意味着,增加迭代器的副本不会使原始迭代器失效。也就是说,执行prev = it; ++it不会使prev失效,因此您的算法是定义良好的。然而,并非所有迭代器都是如此。InputIterator概念并不提供此保证。请注意++i的后置条件。我不知道标准库中是否有任何迭代器在增加其副本时会丢失值,但我个人构建过这样的迭代器类型(它遍历归档文件中的条目,当你前进到下一个条目时,前一个条目将不再可读)。

另请参阅IteratorForwardIteratorRandomAccessIteratorOutputIterator的概念。它们都是相互建立的。


1
std::istream_iterator 是一种“自毁”的迭代器之一。 - Quentin
1
递增迭代器的副本不会使原始迭代器失效。太好了,这正是我需要的保证。我知道迭代器类别,但我没有意识到这个属性在其中得到了明确说明,也没有意识到有些类别没有这个属性。(这很合理——你提到的存档示例很好——这让我感觉更放心,因为我没有过度谨慎。) - Steve Summit
@Quentin 我认为 std::istream_iterator 仍然有效,但没有提供 ForwardIterator 的多遍保证的其余部分。 - Miles Budnek
@SteveSummit 注意不要在调用之间重新调整容器的大小。 - rioki
这个问题现在给我带来了一些小麻烦。我编写了一个自定义输出迭代器,仅用于单次使用。但根据所需求的 LegacyOutputIterator 命名要求,我应该支持复制,即使对于我的迭代器没有意义,特别是因为它会在结束时导致刷新输出的麻烦!(我的迭代器将位映射到字节,将另一个输出迭代器包装到字节流中,并在写入足够的位时“刷新”,并在销毁时刷新任何剩余的位)。我已经提供了只有移动语义的解决方案。 - saxbophone

0
虽然没有明确的规定,但我通常会建议不要存储迭代器。
首先,迭代器的概念是为了抽象你所访问的内容。这可以是一个流或者一个容器。流迭代器肯定是不稳定的,存储它们将导致无效的代码。但是,只要容器不改变,将迭代器存储到容器中是安全的。
第二个问题是,如果你正在访问一个容器并且只是从中读取数据,那么你的代码是正确的。如果你的代码在调用之间修改了容器,那么你的迭代器可能会失效。不同的容器在迭代器失效时有不同的语义,但我的经验法则是一旦容器被重新调整大小,所有的迭代器都将失效。
(我不倾向于记住精确的语义,因为我假设我随时可能更改容器,而且当一个容器调整大小时,向量会使所有的迭代器失效。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接