在基于范围的 for 循环中将元素添加到该向量上的预分配向量是不是合法?

Posted

技术标签:

【中文标题】在基于范围的 for 循环中将元素添加到该向量上的预分配向量是不是合法?【英文标题】:Is it legal to add elements to a preallocated vector in a range-based for loop over that vector?在基于范围的 for 循环中将元素添加到该向量上的预分配向量是否合法? 【发布时间】:2016-02-17 21:32:50 【问题描述】:

我正在使用 Visual Studio 2015 Update 1 C++ 编译器和这段代码 sn-p:

#include <iostream>
#include <vector>

using namespace std;

int main()

  vector<int> v3, 1, 4;

  v.reserve(6);

  for (auto e: v)
    v.push_back(e*e);

  for (auto e: v)
    cout << e << " ";

  return 0;

发布版本运行良好,但调试版本产生vector iterators incompatible 错误消息。这是为什么呢?

在将其标记为与Add elements to a vector during range-based loop c++11 重复的问题之前,请阅读我的回答 https://***.com/a/35467831/219153 有相反的论据。

【问题讨论】:

这是未定义的行为,它可能有效,也可能无效。在调试版本中它不起作用。 关于标题,它可以适用于如此广泛的问题。您应该考虑找出与问题内容密切匹配的标题。例如,可能类似于“在基于范围的 for 循环中将元素添加到该向量上的预分配向量是否合法?”我会让你选择你认为最合理的。 @chris 你对标题是对的,我编辑了它。我一开始没有使用类似头衔的原因纯粹是出于政治考虑。我发现 SO 编辑在将问题标记为重复并忽略任何未来的相反论点时太高兴了。我想绕过它。 @PaulJurczak,我可以看到你来自哪里。有时即使问题不应该关闭,也很难打开它。虽然这些更新的更改允许带有金标签徽章的帐户在某人开枪时进行某些事情可能会受到伤害,但它们也允许一票重新打开以关闭欺骗。这应该特别有助于解决重复问题。 【参考方案1】:

根据documentation:

如果新的 size() 大于 capacity() 则所有迭代器和 引用(包括过去的迭代器)无效。 否则只有过去的迭代器无效。

它说即使容量足够,结束后的迭代器也会失效,所以我相信你的代码有未定义的行为(除非这个文档不正确并且标准另有规定)

【讨论】:

文档是正确的。不能使用无效的迭代器对向量进行范围循环。 我并不是说标准可以说对无效迭代器进行范围循环是可以的,我的意思是结束迭代器无效是正确的说法。 不仅如此。根据标准,基于范围的 for 循环在循环开始之前获取结束迭代器。一旦你将一个元素添加到向量中,结束迭代器已经改变,现在你有一个迭代器不匹配。【参考方案2】:

您的代码表现出未定义的行为,但它很棘手,而且往往只会在调试版本中被发现。

当您执行v.push_back 时,如果大小超过容量,则所有迭代器都将失效。你可以通过保留来避免这种情况。

但是,即使您没有导致容量增长,过去的迭代器仍然无效。一般来说,迭代器失效规则不区分“迭代器的'值'将是垃圾/引用不同的对象”和“迭代器的'位置'不再有效”。当任何一种情况发生时,迭代器都被简单地视为无效。由于结束迭代器不再是结束迭代器(几乎在每个实现中,它都从无引用变为引用某物),标准只是声明它是无效的。

这段代码:

for (auto e: v)
  v.push_back(e*e);

大致扩展为:


  auto && __range = v; 
  for (auto __begin = v.begin(),
    __end = v.end(); 
    __begin != __end;
    ++__begin
  )
   
       auto e = *__begin;
       v.push_back(e*e);
  

v.push_back 调用使__end 迭代器无效,然后与之进行比较,并且调试构建正确地将未定义的行为标记为问题。调试 MSVC 迭代器对失效规则非常小心。

发布版本的行为未定义,因为向量迭代器基本上是指针周围的薄包装,并且指向过去元素的指针在没有容量溢出的情况下推回后变成指向最后一个元素的指针,它“有效”。

【讨论】:

将其设为未定义行为是否有优势?它迫使我使用指针或索引而不是使用更高级别的构造来重写此循环。最终结果是编译器会从我的示例中生成相同的代码,但语言律师会认为是非法的。这是 STL 设计的缺陷吗? @PaulJurczak 为了做你想做的事,他们必须在迭代器上添加一个半失效效果,并准确指定它的含义和发生时间。那将是一个仍然有效的迭代器,但现在引用不同的元素。有了这个概念,你可以很容易地表示,当元素被追加并且容量没有增加时,结束迭代器成为旧最后一个元素之后的元素的迭代器。至少,这会使标准更加复杂。当有人插入向量的中间时,您可能最终也想做同样的事情。好主意?不知道。 试图窥探规范编写者的想法,实现者可能希望拥有一个结束迭代器,其值是一个标记,而不仅仅是指向内存中下一个点的指针。如果他们指定所有以前的结束迭代器都指向新的push_backed 值,那么这样的实现将是不可能的。为什么,值得吗?我不知道。也许调试功能可能会发现将迭代器标记为过去是很有价值的。规范编写者倾向于在未定义行为方面犯错,并在需要时稍后定义。 @CortAmmon past-the-end 迭代器,其值为哨兵,而不仅仅是指向内存中下一个点的指针 也许,但这会违反“零成本”抽象”规则,通常用 C++ 宣传。递增指针是访问连续向量元素的最有效方式。这需要过去的迭代器指向最后一个元素之后的一个元素。这是任何启用了速度优化的编译器都会将迭代器减少到的值。 @PaulJurczak 也许这是一个经验法则。我可以看到使用此类标记的一个特殊情况是在调试版本中,类似于 Visual Studio 运行时迭代器检查。当然,他们不使用哨兵,但它可能很有用。更重要的是,如果他们在规范中禁止这样做,那么没有人可以这样做。

以上是关于在基于范围的 for 循环中将元素添加到该向量上的预分配向量是不是合法?的主要内容,如果未能解决你的问题,请参考以下文章

为啥需要两个范围 for 循环来更改 C++ 中向量的这些元素?

考虑到干净代码的性能,啥更好

C++基于范围的for循环详解

临时范围上的基于范围的for循环[重复]

第9课 基于范围的for循环

将类添加到对象1,等待,在for循环中将类添加到对象2