Unordered_map元素被删除

unordered_map element being deleted

本文关键字:删除 元素 map Unordered      更新时间:2023-10-16

我将{string, MyStruct}对象插入到unordered_map中,稍后遍历unordered_map并选择擦除该元素。但是,在擦除元素之前,我有一个断言,显示unordered_map为空。

这是我的插入:

my_umap.insert(std::make_pair(key.toString(), my_struct));

结构体包含一个记录插入时间的成员。然后定期检查映射并删除在unordered_map中存在时间过长的元素:

for(auto it = my_umap.begin(); it != my_umap.end(); ++it){
    MyStruct& myStruct = it->second;
    const bool deleteEntry = myStruct.ts.IsElapsed(std::chrono::seconds(5));
    if(deleteEntry){
        const string& key = it->first;    // Cannot access memory for 'key'
        assert(my_umap.size() >= 1);       // This is failing
        my_umap.erase(key);
    }
}

我在gdb中运行代码,断言失败。当我查询key的值时,它显示

无法访问内存

当我查询my_umap的大小时,它显示大小为0。

如果unordered_map的大小为零,for循环如何检测元素?没有其他线程访问这个容器。我认为unordered_map::insert()将对象复制到容器中,因此删除原始对象应该无关紧要?

调用my_umap.erase(...)后,迭代器失效:

cppreference.com表示:

对已删除元素的引用和迭代器无效。其他迭代器和引用不会失效。

这意味着一旦该元素被擦除,指向它的迭代器就不再有效。

你有几个选择:

<标题> 1。使用迭代器擦除,并使用erase() 的返回值

从c++ 11开始,通过迭代器擦除将返回一个指向映射中的下一项的迭代器。所以你可以用这个来使你的迭代器有效:

auto it = my_umap.begin();
while (it != my_umap.end()) {
    MyStruct& myStruct = it->second;
    const bool deleteEntry = myStruct.ts.IsElapsed(std::chrono::seconds(5));
    if(deleteEntry){
        assert(my_umap.size() >= 1);
        it = my_umap.erase(it);  // <-- Return value should be a valid iterator.
    }
    else{
        ++it;  // Have to manually increment.
    }
}
<标题> 2。将迭代器存储在list对象中,迭代后擦除。

或者,您可以将删除候选对象存储在列表对象中(例如vector),并在初始迭代后删除它们:

std::vector<MapType::iterator> deleteCandidates;
for(auto it = my_umap.begin(); it != my_umap.end(); ++it){
    MyStruct& myStruct = it->second;
    const bool deleteEntry = myStruct.ts.IsElapsed(std::chrono::seconds(5));
    if(deleteEntry)
        deleteCandidates.push_back(it);
}
for (auto it : deleteCandidates) {
    my_umap.erase(it);
}

至于你的断言失败的原因,你可能会遇到未定义的行为,通过访问无效的迭代器,使你的for循环认为映射仍然不是空的(因为invalidIterator != my_umap.end())。

erase()使要擦除的迭代器失效。当您随后在for循环中增加它时,您将获得未定义的行为。assert()可能在循环的第二次迭代时触发,而不是第一次。

你必须重新构造你的循环,像

for(auto it = my_umap.begin(); it != my_umap.end(); /* nothing */){
    MyStruct& myStruct = it->second;
    const bool deleteEntry = myStruct.ts.IsElapsed(std::chrono::seconds(5));
    if(deleteEntry) {
        // either use the return
        it = my_umap.erase(it); // NB: erase by it, not by key, why
                                // do an extra lookup?       
        // or post-increment
        my_umap.erase(it++);
    }
    else {
        ++it;
    }
}

就我个人而言,我更喜欢it = map.erase(it)而不是map.erase(it++)

我只是把它包装在一个函数模板中,这样你就不必一直重写这类东西:

template <class Container, class F>
void erase_if(Container& c, F&& f) {
    for (auto it = c.begin(); it != c.end(); ) {
        if (f(*it)) {
            it = c.erase(it);
        }
        else {
            ++it;
        }
    }
}

然后:

erase_if(my_umap, [](const auto& pr){
    MyStruct& myStruct = pr.second; 
    return myStruct.ts.IsElapsed(std::chrono::seconds(5));
});

实际上,@barry建议的erase_if()已经是TS Library Fundamentals V2 (Uniform Container Erasure)的一部分。我现在还不知道它是否会成为c++ 17的一部分。

这是一个参考:http://en.cppreference.com/w/cpp/experimental/unordered_map/erase_if

Visual Studio 2015附带的标准库已经实现了它。

libstdc++ (gcc附带的标准库)的状态页我们可以看到它也实现了它,但它只在开发版本中,而不是在任何特定的发行版中。(https://gcc.gnu.org/onlinedocs/libstdc + +/手册/status.html # status.iso.201z)

请注意,与当前大多数算法不同,这些函数位于相关容器的头文件中(而不是算法头文件中),并且它们引用容器本身(而不是一对迭代器)。这些变化是因为这些函数必须了解容器才能正确实现循环并使用容器成员函数erase()。因此,如果您希望仅对容器中的特定范围应用此擦除操作,则仍然必须使用其他答案中描述的手写循环(可能Ranges TS也改进了这一点?)。

问题是你把迭代器弄乱了。在迭代过程中擦除元素,等于擦除了本应指向下一个迭代器的当前迭代器。

有几种方法可以解决这个问题。第一个是我刚从c++参考中复制的东西:

int main()
{
    std::map<int, std::string> c = { { 1, "one" }, { 2, "two" }, { 3,     "three" },
    { 4, "four" }, { 5, "five" }, { 6, "six" } };
    // erase all odd numbers from c
    for (auto it = c.begin(); it != c.end();)
        if (it->first % 2 == 1)
            it = c.erase(it);
        else
            ++it;
    for (auto& p : c)
        std::cout << p.second << ' ';

注意循环。它不会推进迭代器。相反,它将迭代器赋值给被擦除元素返回的迭代器——erase返回下一个迭代器,或者,如果不擦除,则显式推进该迭代器。

解决这个问题的第二个选项是我对第一个程序所做的以下更改:

int main()
{
    std::map<int, std::string> c = { { 1, "one" }, { 2, "two" }, { 3, "three" },
{ 4, "four" }, { 5, "five" }, { 6, "six" } };
    // collect all odd numbers from c into a vector
    vector<int> to_delete;
    for (auto pair : c) if (pair.first % 2 == 1) to_delete.push_back(pair.first); 
    // now delete them all
    for (auto k : to_delete) c.erase(k);
    for (auto& p : c)
        std::cout << p.second << ' ';
}

这次我收集了一个向量中的所有密钥,然后扫描该向量并从映射中擦除每个密钥。这样在迭代映射时就不会从映射中擦除。