【问题标题】:Decrementing std::vector::iterator before std::vector::begin()在 std::vector::begin() 之前递减 std::vector::iterator
【发布时间】:2017-07-03 10:12:39
【问题描述】:

遵循向量上的“迭代时删除”模式之一,我不明白为什么这段代码有效,或者它是否使用了未定义的行为:

守则

#include <vector>
#include <iostream>

int main(int argc, char* argv[], char* envz[])
{
  std::vector<std::string> myVec;
  myVec.push_back("1");
  myVec.push_back("2");
  myVec.push_back("3");

  for (std::vector<std::string>::iterator i = myVec.begin();
       i != myVec.end();
       ++i)
  {
    if ("1" == *i)
    {
      std::cout << "Erasing " << *i << std::endl;
      i = myVec.erase(i);
      --i;
      continue;
    }
    std::cout << *i << std::endl;
  }

  return 0;
}

输出

>g++ -g main.cpp
>./a.out 
Erasing 1
2
3

问题

考虑 for 循环的第一次迭代:

  • i myVec.begin(),它“指向”1
  • 我们进入条件块。
  • 1 被擦除,i 被设置为被擦除元素的后一位,即2,现在 myVec.begin() 也指向它
  • 我递减i,所以现在它指向... myVec.begin() 之前的一个???

我很困惑为什么这似乎有效,正如输出所证明的那样,但是递减迭代器感觉有些可疑。如果条件为if ("2" == *i),则此代码很容易合理化,因为迭代器减量仍将其放置在向量中的有效条目处。 IE。如果我们有条件地删除2i 将被设置为指向3,但随后手动递减并因此指向1,然后是for循环递增,将其设置为指向3再次。有条件地擦除最后一个元素同样容易理解。

我还尝试了什么

这个观察让我假设在 vector::begin() 之前递减是幂等的,所以我尝试添加一个额外的递减,如下所示:

#include <vector>
#include <iostream>

int main(int argc, char* argv[], char* envz[])
{
  std::vector<std::string> myVec;
  myVec.push_back("1");
  myVec.push_back("2");
  myVec.push_back("3");

  for (std::vector<std::string>::iterator i = myVec.begin();
       i != myVec.end();
       ++i)
  {
    if ("1" == *i)
    {
      std::cout << "Erasing " << *i << std::endl;
      i = myVec.erase(i);
      --i;
      --i;      /*** I thought this would be idempotent ***/
      continue;
    }
    std::cout << *i << std::endl;
  }

  return 0;
}

但这导致了段错误:

Erasing 1
Segmentation fault (core dumped)

有人可以解释为什么第一个代码块有效,特别是为什么擦除第一个元素后的单个减量有效吗?

【问题讨论】:

  • 使用未定义行为的东西并不能保证它会崩溃 - 它有时可能会正常工作。
  • 它并没有真正起作用,但未定义行为的一种可能影响是“似乎起作用”。如果--i; ++i; 导致i 保持不变,这完全不足为奇。
  • 为什么你会认为--i 是幂等的?!无论如何,如果您只是尝试使用支持调试迭代器的实现 like this 并看到它中止并显示有用的错误消息,您将节省大量时间。
  • @JonathanWakely 只是为了回答你的问题,我想知道--i 在指向vector::begin() 时是否是幂等的,或者它是否指向某个神奇的“pre-begin()”持有点来自它可以合法地增加到begin()。但这既不存在也不存在:这段代码似乎依赖于未定义的行为。

标签: c++ vector iterator


【解决方案1】:

不,您的代码具有未定义的行为:如果i == myVec.begin(),则i = myVec.erase(i); 导致i 再次成为myVec.begin() 的(新值),而--i 具有未定义的行为,因为它超出了有效范围迭代器的范围。

如果您不想使用擦除删除习语(即myVec.erase(std::remove(myVec.begin(), myVec.end(), "1"), myVec.end())),则手动循环时变异如下所示:

for (auto it = myVec.begin(); it != myVec.end(); /* no increment! */) {
  if (*it == "1") {
    it = myVec.erase(it);
  } else {
    ++it;
  }
}

无论如何,这里和您的原始代码中的关键点是erase 使迭代器无效,因此必须在擦除后为迭代器重新分配一个有效值。这要归功于erase 的返回值,这正是我们需要的新的、有效的迭代器。

【讨论】:

  • 不,正如我的回答所述,这是由于 ++ifor 循环完成的。
  • 但是擦除值所在的位置已经没有了,所以i之前没有任何东西,所以--i是未定义的。因为它超出了有效范围。听起来您正试图围绕未定义的行为争论。你不能那样做。它未定义,停止它。
  • @Kerrek,我同意您的代码示例是处理这种情况的更可靠的方法,但我仍然很好奇我的原始代码出了什么问题。我认为您+其他人的答案表明问题是由于递减过去 vector::begin() 的未定义行为。
  • @JonathanWakely - 不要试图“围绕未定义的行为争论”。试图确认未定义的行为是这里发生的事情。这是我的怀疑,所以我正在向社区寻求确认。
  • 尝试打开调试检查:coliru.stacked-crooked.com/a/d07c9bcb2d285a54(您会更快得到答案)。
【解决方案2】:

这可能在某些编译器中有效,但在其他编译器中可能会失败(例如,编译器实际上可能会在运行时检查您没有在 begin() 下递减并在这种情况下抛出异常 - 我相信至少有一个编译器会这样做,但是不记得是哪一个了)。

在这种情况下,一般模式不是在 for 中递增,而是在循环内递增:

  for (std::vector<std::string>::iterator i = myVec.begin();
       i != myVec.end();
       /* no increment here */)
  {
    if ("1" == *i)
    {
      std::cout << "Erasing " << *i << std::endl;
      i = myVec.erase(i);
      continue;
    }
    std::cout << *i << std::endl;
    ++i;
  }

使用向量,错误的迭代实际上可能在更多情况下有效,但如果你尝试这样做,你会遇到非常糟糕的时间,例如std::mapstd::set

【讨论】:

    【解决方案3】:

    这里的关键是递减后的continue。 通过调用它,++i 将在取消引用 i 之前由循环迭代触发。

    【讨论】:

    • 这是不安全的,这是未定义的行为。在许多平台上什么都不会发生,但在其他平台上它会崩溃(例如,一些标准库在调试模式下进行边界检查)。
    • 为什么会这样?我不明白如何为指针未定义
    • 与未定义迭代器的原因完全相同
    • 好吧,我不太明白,但我删除了那部分。但我认为没有人回答最初的问题:Can someone explain why the first code bock works, and specifically why the single decrement after erasing the first element is valid?他真的在问为什么,不仅想知道它是否“好”
    • @Ninetainedo - 我对第一个和第二个代码块的疑惑是正确的。但我认为答案归结为未定义的行为。
    猜你喜欢
    • 2014-12-05
    • 2022-01-19
    • 2012-12-20
    • 1970-01-01
    • 2017-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多