c++ - 为什么删除元素的 std::for_each 不会中断迭代?

标签 c++ c++11

据我所知,在集合迭代期间删除元素会破坏迭代或导致您跳过元素。为什么使用删除的谓词调用 std::for_each 不会导致这种情况发生? (有效)。

代码片段:

#include <iostream>
#include <map>
#include <algorithm>

using namespace std;

int main() {
    map<int,long long> m;
    m[1] = 5000;
    m[2] = 1;
    m[3] = 2;
    m[4] = 5000;
    m[5] = 5000;
    m[6] = 3;

    // Erase all elements > 1000
    std::for_each(m.begin(), m.end(), [&](const decltype(m)::value_type& v){
        if (v.second > 1000) {
            m.erase(v.first);
        }
    });

    for (const auto& a: m) {
      cout << a.second << endl;
    }
    return 0;
}

打印出来

1
2
3

编辑:我现在看到 if 它实际上在调用函数之前递增迭代器然后它可以工作。但这是否算作编译器特定/未定义的行为?

最佳答案

这是未定义的行为,不会可靠地工作。在删除 lambda 函数中添加一行以打印键和值后,我看到:

1=5000
2=1
3=2
4=5000
2=1          // AGAIN!!!
3=2          // AGAIN!!!
5=5000
6=3

使用我的标准库的 map 实现,在删除键为 4 的元素后,迭代返回键为 2 的节点!然后它重新访问具有键 3 的节点。因为您的 lambda 愉快地重新测试了这些节点 (v.second > 1000) 并且返回时没有任何副作用,所以中断的迭代不会影响输出。

您可能会合理地问:“但从天文角度来看,它在不崩溃的情况下设法继续迭代(即使到错误的下一个节点)是不可能的吗?”

实际上,很有可能。

删除节点会导致为该节点占用的内存调用delete,但通常执行delete的库代码只会:

  • 调用析构函数(没有特别的理由浪费时间覆盖左子指针、右子指针和父指针),然后

  • 修改其关于已分配内存区域与可用内存区域的记录。

不太可能“浪费”时间任意修改正在释放的堆内存(尽管某些实现会处于内存使用 Debug模式)。

因此,被删除的节点可能原封不动地坐在那里,直到执行其他一些堆分配。

并且,当您erase map 中的一个元素时,标准要求容器中的任何其他元素都不能在内存中移动 - 迭代器、指针和对其他元素的引用元素必须保持有效。它只能修改维护二叉树的节点的左/右/父指针。

因此,如果您继续对已删除元素使用迭代器,则很可能会看到指向已删除元素在删除之前链接到的左/右/父节点的指针,以及 operator++()将使用与删除的元素仍在 map 中时所采用的相同逻辑“迭代”它们。

如果我们考虑示例 map 的内部二叉树,其中 N3 是具有键 3 的节点:

                           N5
                          /  \
                        N3    N7
                       / \    /
                      N1  N4 N6

完成迭代的方式可能是:

  • 最初,从 N1 开始; map 必须直接跟踪它的位置以确保 begin() 是 O(1)

  • 如果在一个没有子节点的节点上,重复 { Nfrom = where you are, move to parent, if nullptr or right != Nfrom break} (e.g. N1->N3, N4->N3->N5, N6->N7->N5->nullptr)

  • 如果在一个有右手 child 的节点上,那么取它任意数量的左手链接(例如 N3->N4,N5->N7->N6)

所以,如果说 N4 被移除(所以 N3->right = nullptr;)并且没有重新平衡发生,那么迭代记录 NFrom=N4 然后移动到父 N3,然后 N3->right != Nfrom,所以它会认为它应该停止在已经迭代的 N3 上,而不是继续前进到 N5。

另一方面,如果在 erase 之后树已经重新平衡,则所有赌注都将关闭,无效的迭代器可能会重复或跳过元素,甚至“按希望”迭代。

不是旨在让您推断erase 后的行为 - 它未定义,不应依赖。相反,我只是想展示一个理智的实现可以解释您的意外观察结果。

关于c++ - 为什么删除元素的 std::for_each 不会中断迭代?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21977712/

相关文章:

c++ - 完全限定的静态成员定义不会编译

c++ - `tellp` 和 `tellg` 文件打开和打开时

c++ - 如何避免堆分配将 Rendercommands 插入到 RenderCommandBuffer?

c++ - 识别标识符不工作的派生类

c++ - 使用静态成员初始化静态映射

c++ - 在代码::Blocks中更改类变量大小写

c++ - 什么语言规则允许 C++11 推断这是一个 initializer_list 对?

c++ - 是什么导致代码在 GPU 中运行?

c++ - 桌面/屏幕视频采集/录像库 "pc not mobile "[c++/Qt]

c++ - 如何将全局变量传递给 C++ 中的函数?