【问题标题】:Bug with std::deque?std::deque 的错误?
【发布时间】:2015-12-12 03:48:30
【问题描述】:

我正在尝试使用循环和迭代器从双端队列中删除一个元素。我正在关注online examples,但发现了一个错误。

我正在使用 g++ (GCC) 4.8.3 20140911 (Red Hat 4.8.3-9)。

代码如下:

#include <iostream>
#include <deque>

using namespace std;

// Display the contents of a queue
void disp_deque(deque<int>& deque) {
  cout << "deque contains: ";
  for (auto itr = deque.begin(); itr!=deque.end(); ++itr)
    cout << *itr << ' ';
  cout << '\n';
}

int main(int argc, char** argv) {
  deque<int> mydeque;

  // Put 10 integers in the deque.
  for (int i=1; i<=10; i++) mydeque.push_back(i);
  disp_deque(mydeque);

  auto it = mydeque.begin(); 
  while (it!=mydeque.end()) {
    cout << "Checking " << *it << ',';
    // Delete even numbered values.
    if ((*it % 2) == 0) {
      cout << "Deleting " << *it << '\n';
      mydeque.erase(it++);
      disp_deque(mydeque);
    } else ++it;
  }
}

这很简单 - 创建一个包含 10 个元素的列表并删除偶数个。

注意以下事项(绒毛除外):

if ((*it % 2) == 0) {
  mydeque.erase(it++);
} else it++;

建议使用迭代器进行删除,这样您的迭代器就不会像上面链接中提到的那样失效。

但是,当我运行它时,我得到以下信息:

$ ./test
deque contains: 1 2 3 4 5 6 7 8 9 10 
Checking 1,Checking 2,Deleting 2
deque contains: 1 3 4 5 6 7 8 9 10 
Checking 3,Checking 4,Deleting 4
deque contains: 1 3 5 6 7 8 9 10 
Checking 5,Checking 6,Deleting 6
deque contains: 1 3 5 7 8 9 10 
Checking 7,Checking 8,Deleting 8
deque contains: 1 3 5 7 9 10 
Checking 10,Deleting 10
deque contains: 1 3 5 7 9 
Checking 10,Deleting 10
deque contains: 1 3 5 7 
Checking 0,Deleting 0
deque contains: 1 3 5 
Checking 0,Deleting 0
deque contains: 1 3 
Checking 0,Deleting 0
deque contains: 1 
Checking 0,Deleting 0
deque contains: 
Checking 0,Deleting 0
Segmentation fault (core dumped)

查看它,直到它删除 8 之前,它似乎还不错。实际上,数字 9 被完全跳过并且从未检查过!我期望发生的事情是这样的:

$ ./test
deque contains: 1 2 3 4 5 6 7 8 9 10 
Checking 1,Checking 2,Deleting 2
deque contains: 1 3 4 5 6 7 8 9 10 
Checking 3,Checking 4,Deleting 4
deque contains: 1 3 5 6 7 8 9 10 
Checking 5,Checking 6,Deleting 6
deque contains: 1 3 5 7 8 9 10 
Checking 7,Checking 8,Deleting 8
deque contains: 1 3 5 7 9 10 
Checking 9,Checking 10,Deleting 10
deque contains: 1 3 5 7 9 

事实上,这正是我将代码更改为这个时得到的:

if ((*it % 2) == 0) {
  it=mydeque.erase(it);
} else it++;

那么,为什么一种方法有效,而另一种无效?谁能解释一下?

即使我创建了一个要删除的临时迭代器,我也会看到完全相同的问题输出:

  while (it!=mydeque.end()) {
    cout << "Checking " << *it << ',';
    auto tmp_it = it++;
    // Delete even numbered values.
    if ((*tmp_it % 2) == 0) {
      cout << "Deleting " << *tmp_it << '\n';
      cout << "IT before delete: " << *it << '\n';
      mydeque.erase(tmp_it);
      cout << "IT after delete: " << *it << '\n';
      disp_deque(mydeque);
    } 
  }

在这里,我将它的副本存储在 tmp_it 中,然后将其递增。我添加了一些调试语句,并看到了一些非常奇怪的东西:

...
deque contains: 1 3 5 6 7 8 9 10 
Checking 5,Checking 6,Deleting 6
IT before delete: 7
IT after delete: 7
deque contains: 1 3 5 7 8 9 10 
Checking 7,Checking 8,Deleting 8
IT before delete: 9
IT after delete: 10
deque contains: 1 3 5 7 9 10 
Checking 10,Deleting 10
IT before delete: 10
IT after delete: 10
...

但是,删除元素 8 使它指向元素 10,跳过了 9!在之前的删除中,它指向前一个元素(例如,当 6 被删除时,它在删除之前和之后都指向 7)。

我查看了deque 的实现,并在“迭代器有效性”下看到以下内容(强调我的):

迭代器有效性如果擦除操作包括最后一个元素 在序列中,结束迭代器和迭代器、指针和 引用已擦除元素的引用无效。如果 擦除包括第一个元素但不包括最后一个元素,只有那些 指被擦除的元素是无效的。 如果发生 双端队列中的任何其他地方,所有迭代器、指针和引用 与容器相关的无效。

这是否意味着在我的代码中,即使我在删除之前对其进行了后期增量,我的迭代器也会失效?即我删除的迭代器以外的迭代器正在失效?

如果是这样,那很好,但这似乎是一个鲜为人知的错误。这意味着common在循环中删除迭代器的实现在使用双端队列时无效。

【问题讨论】:

  • auto next_iterator = queue.erase(it):(请查阅文档)
  • 您引用的示例仅适用于基于节点的容器,例如 setlist
  • @TemplateRex 我开始看到了。不幸的是,使用迭代器的全部意义在于它是一种通用的循环方法,而与容器无关。太糟糕了,事实并非如此。谢谢!
  • @Trenin 是的,这很不幸,因此我在回答中引用了 Effective STL 中关于它的项目:)

标签: c++ c++11 iterator deque


【解决方案1】:

来自deque::erase()上的cppreference:

所有迭代器和引用都无效,除非被擦除的元素位于容器的末尾或开头,在这种情况下,只有迭代器和对被擦除元素的引用无效。

所有迭代器。他们都是。当你这样做时:

mydeque.erase(it++);

您发布增量it 并不重要,新的迭代器也会失效。这正是erase() 返回的原因:

最后一个被移除元素之后的迭代器。如果迭代器 pos 引用最后一个元素,则返回 end() 迭代器。

这样你就可以做到:

it = mydeque.erase(it); // erase old it, new it is valid

虽然更好的办法是通过使用擦除删除习语来完全避免这种错误来源:

mydeque.erase(
   std::remove_if(mydeque.begin(), mydeque.end(), [](int i){return i%2 == 0; }),
   mydeque.end()
);

有关迭代器失效的更多信息,另请参阅this question

【讨论】:

    【解决方案2】:

    您引用的代码仅适用于关联容器setmap 等)。

    Scott Meyers 的 Effective STL 第 9 项(恰当地命名为“在擦除选项中谨慎选择”)展示了它是如何为 序列容器(向量、双端队列、字符串)完成的

    for (SeqContainer<int>::iterator it = c.beqin(); it != c.end();) {
        if (predicate(*it)){
            it = c.erase(it); // keep it valid by assigning
        }                     // erase's return value to it
        else ++it;
    }
    

    这里,erase() 返回值正是我们所需要的:它是一个 擦除完成后,指向被擦除元素之后的元素的有效迭代器。

    【讨论】:

    • 我认为这个回答最好。谢谢!
    • @Trenin 这解释了老式的解决方案,@AlexanderBessenov 展示了通用的erase_if 算法将如何在未来解决它。
    • 是的,但该方法仅适用于我在这里强调的琐碎任务;即循环容器以删除某些元素。如果您在循环中做更多事情(例如,有时删除,有时添加,有时根据容器的内容进行修改),那么这样的方法是一种更好、更通用的方法。
    【解决方案3】:

    这是否意味着在我的代码中,即使我在删除之前对其进行了后期增量,我的迭代器也会失效?

    这正是它的意思。该失效是否对您的结果有任何影响,这取决于您的运行时库中dequeue 的实现。它也可能在许多情况下运行良好,然后突然失败,比如你的 8。

    后自增技巧仅适用于容器,其中erase 仅使擦除元素的迭代器无效(如lists 和sets)。在这些情况下,后增量会为 next 元素创建一个迭代器,并在 before 元素被擦除之前这样做。 next 元素的迭代器因此不受与擦除相关的失效的影响。然而,在 dequeue 的情况下,规范说 所有 迭代器都无效。

    【讨论】:

      【解决方案4】:

      C++14 引入了通用erase_if 算法,该算法适用于所有类型的标准容器。

      http://en.cppreference.com/w/cpp/experimental/deque/erase_if

      这相当于@Barry提供的最后一个代码块:

      #include <experimental/deque>
      std::experimental::erase_if(mydeque, [](int i){return i%2 == 0; });
      

      与直接擦除/删除-if 模式相比,使用这种通用算法也更好,因为如果您决定将容器替换为 std::set,例如,您将不需要更新处理删除的代码.

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-01-15
        • 2023-03-14
        • 2012-09-23
        • 1970-01-01
        • 1970-01-01
        • 2016-02-26
        • 2013-04-10
        • 2021-09-10
        相关资源
        最近更新 更多