std::remove 与 vector::erase 和未定义的行为

Posted

技术标签:

【中文标题】std::remove 与 vector::erase 和未定义的行为【英文标题】:std::remove with vector::erase and undefined behavior 【发布时间】:2014-07-08 19:15:00 【问题描述】:

在整个网络上,我看到人们将erase/remove idiom 用于 C++ 向量,如下所示:

#include <vector> // the general-purpose vector container
#include <iostream>
#include <algorithm> // remove and remove_if
int main()

  // initialises a vector that holds the numbers from 0-9.
  std::vector<int> v =  0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ;

  // removes all elements with the value 5
  v.erase( std::remove( v.begin(), v.end(), 5 ), v.end() );

  return 0;

也就是说,如果我想删除所有符合某些条件的元素(例如,ints 向量中的数字 5),那么我将 std::removestd::remove_ifvector.erase 结合使用,如下所示:

vector.erase( std::remove( vector.begin(), vector.end(), <some_value>), vector.end());

总的来说,这很好用; std::remove(和 remove_if)将复制(或在 C++11 中使用移动语义)要删除的元素到向量的末尾,因此我们之前示例中的向量现在看起来像这样:

0, 1, 2, 3, 4, 6, 7, 8, 9, 5 ;

元素 5 加粗,因为它已移到末尾。

现在,std::remove 将返回一个迭代器,然后我们在 erase 中使用它来清除元素。不错。

但是下面的例子呢?

int main()

  // initialises an empty vector.
  std::vector<int> v = ;

  // removes all elements with the value 5
  v.erase( std::remove( v.begin(), v.end(), 5 ), v.end() );

  return 0;

这似乎在我运行它的所有平台上都按预期工作(不删除任何内容,不进行段错误等),但我知道仅仅因为某些东西在工作,并不意味着它不是未定义的行为。

vector.erase 的快速 reference 是这样说的(强调我的):

iterator erase (const_iterator first, const_iterator last);

first, last

在向量内指定范围的迭代器] 要删除:[first,last)。即范围包括firstlast之间的所有元素,包括first指向的元素,但不包括last指向的元素。 成员类型iteratorconst_iterator 是指向元素的随机访问迭代器类型。

vector.erase(vector.end(),vector.end()) 是未定义的行为吗?

以下是快速参考中关于异常安全的内容:

如果移除的元素包括容器中的最后一个元素,则不会抛出异常(保证不抛出)。 否则,保证容器以有效状态结束(基本保证)。 无效的 positionrange 会导致未定义的行为。

所以,至少在我看来答案是“是”,this *** answer 似乎支持它。

所以,习语有错吗?

假设它是未定义的行为,那么任何对 remove 的调用都可能返回一个迭代器到 vector.end(),在调用 vector.erase 之前应该检查它,并且在空向量上调用 remove 似乎返回 vector.end: (@ 987654324@)

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main() 
   vector<int> myInts;
   auto anIter = std::remove(myInts.begin(),myInts.end(),5);
   if (anIter == myInts.end())
      std::cout << "iterator = myInts.end()";

最后,我的问题:

实际的删除/擦除习语应该是这样吗?

auto endOfRangeIterator = std::remove(vector.begin(), vector.end(), <value>);
if (endOfRangeIterator != vector.end())
   vector.erase(endOfRangeIterator, vector.end())

【问题讨论】:

不,空范围也可以。 [first, first) 是一个空集。 @privatedatapublicchannel2:实际上,引用不正确。在first==last 的情况下,引用自相矛盾,指定一个元素被删除,但没有被删除。这就解释了为什么 AndyG 开始相信这是未定义的行为。 @privatedatapublicchannel2:不一样。标准实际上并没有说i 指向的元素包含在范围内。它只是说这是范围的开始。但是,它确实说j 指向的元素是not 包括在内的。此外,传递给erase 的第二个迭代器不一定是结束迭代器,所以如果first==last,这并不一定意味着第一个指向结束。 @privatedatapublicchannel2:为了清楚起见,请阅读 cplusplus.com 的报价,将“last”替换为“first”。 "包括first指向的元素,但不包括first指向的元素"。在这种情况下,该陈述显然是矛盾的。 最终我们会erase_if 这将简化整个事情。 【参考方案1】:

24.2.1/7 库中大多数对数据结构进行操作的算法模板都有使用范围的接口。范围是一对 指定计算开始和结束的迭代器。 范围[i,i) 是一个空范围;通常,范围[i,j) 指的是数据结构中以元素开头的元素 i 指向的元素,但不包括指向的元素 j.

强调我的。

此外,您引用的erase 的描述不是标准中的规范文本。标准中有这样的说法(表 100):

a.erase(q1,q2)

效果:擦除 [q1, q2) 范围内的元素。

这并不要求q1 是可取消引用的。如果 [q1, q2) 是空范围(根据 24.2.1/7),则范围内没有元素,因此不会删除任何元素。

【讨论】:

感谢您的澄清。根据标准对空范围的定义确实有助于理清思路;否则我认为它会模棱两可。【参考方案2】:

我认为你的引用中更重要的是:

指定向量内范围的迭代器] 被删除: [第一,最后)。即,范围包括第一个之间的所有元素 最后,包括第一个 指向的元素,但不是那个 由最后一个指出。成员类型 iterator 和 const_iterator 是随机的 访问指向元素的迭代器类型。

正如我们在 cmets 中发现的,来自cpluspluc.com 的引用是不正确的。这在( v.end, v.end) 的情况下不会违反规则,但在

的情况下会不正确
#include <vector>

int main()

    std::vector<int> v =  1, 2, 3 ;

    v.erase( v.begin(), v.begin());

因为陈述自相矛盾

范围包括(...),包括所指向的元素 v.begin() 但不是 v.begin() 指向的那个

不能是有效的陈述。

C++ Standard n3337 in § 23.2.2 序列容器要求表 100 指定

a.erase(q1,q2) 返回 iterator 。请注意:

要求:对于vector和deque,T应该是MoveAssignable。效果: 擦除 [q1, q2) 范围内的元素

这就是 § 24.2.1/7 迭代器要求中关于[i,j) 范围的内容

大多数库的算法模板都对数据进行操作 结构具有使用范围的接口。范围是一对 指定计算开始和结束的迭代器。一种 range [i,i) 是一个空范围;一般来说,范围 [i,j) 指的是 数据结构中以指向的元素开头的元素 i 及以上 但不包括 j 所指向的元素。范围 [i,j) 当且仅当 j 可以从 i 到达时有效。结果 将库中的函数应用于无效范围是 未定义。

这样来回答你的问题

但是下面的例子呢?

cplusplus.com 在这种情况下是错误的

vector.erase(vector.end(),vector.end()) 也是未定义的行为吗?

不,不会触发未定义的行为。

所以,习语有错吗?

不,是正确的。

实际的删除/擦除习语应该是这样吗?

没有这个必要,虽然也可以。

【讨论】:

【参考方案3】:

vector.erase(vector.end(),vector.end()) 也是未定义的行为吗?

没有。因为您强调的旁边的声明:

在向量中指定范围的迭代器] 被移除:[first,last)。即范围包括first和last之间的所有元素,包括first指向的元素但不包括last指向的元素

因此,vector.erase(vector.end(),vector.end()) 不会尝试擦除 vector.end(),因为它是由参数 last 指向的。

当然,这个定义是模棱两可的,这些陈述可以被解释为相互矛盾的。标准未使用引用的措辞。

【讨论】:

以上是关于std::remove 与 vector::erase 和未定义的行为的主要内容,如果未能解决你的问题,请参考以下文章

std::remove_extent 可以用来做啥?

为啥 std::remove 需要 const 版本的迭代器? [复制]

令人疑惑的 std::remove 算法

我应该使用 std::remove 从列表中删除元素吗?

std::remove_if 中的 const 参数

有没有更好的替代 std::remove_if 从向量中删除元素的方法?