std::remove_if去除的元素去了哪里?

5

参考资料显示:

template< class ForwardIt, class UnaryPredicate >
ForwardIt remove_if( ForwardIt first, ForwardIt last, UnaryPredicate p );

指向旧端点与新端点之间元素的迭代器仍然有效,但是这些元素本身的值未定义。

我尝试了这个简单的程序,以了解他们所说的"未定义的值"是什么意思。

#include <vector>
#include <memory>
#include <iostream>
#include <algorithm>

int main()
{
    std::vector< std::shared_ptr<int> > ints;
    for (int i = 0; i < 10; ++i)
        ints.push_back(std::make_shared<int>(i));
    std::remove_if(ints.begin(), ints.end(), 
                  [](const std::shared_ptr<int>& element)
                  {
                      return *element % 7 != 0;
                   });
    for (int i = 0; i < 10; ++i)
        std::cout << *ints[i] << std::endl;
    return 0;
}

输出结果如下:
0
7
2
3
4
5
6
The program has unexpectedly finished.

第七个元素后发生了某些神秘的数据问题,导致了段错误。

有趣的是,可能的实现可以在这里找到。

template<class ForwardIt, class UnaryPredicate>
ForwardIt remove_if(ForwardIt first, ForwardIt last, 
                          UnaryPredicate p)
{
    ForwardIt result = first;
    for (; first != last; ++first) {
        if (!p(*first)) {
            *result++ = *first;
        }
    }
    return result;
}

不会产生段错误。

这是一个bug吗?因为迭代器应该是可解引用的。我正在使用gcc 4.7.3。

3个回答

8

首先,如果您不知道,使用 std::removestd::remove_if 时需要记住一件非常重要的事情:它们实际上不能从底层容器中删除元素。这意味着它们本身实际上不会移除任何东西。

您需要使用类似于移除/删除的惯用法:

auto to_erase = std::remove_if(ints.begin(), ints.end(), 
              [](const std::shared_ptr<int>& element)
              {
                  return *element % 7 != 0;
               });
ints.erase(to_erase, ints.end());

“擦除”元素会发生什么是由实现定义的。这里是 gcc 的实现:

  template<typename _ForwardIterator, typename _Predicate>
    _ForwardIterator
    remove_if(_ForwardIterator __first, _ForwardIterator __last,
          _Predicate __pred)
    {
      // concept requirements
      __glibcxx_function_requires(_Mutable_ForwardIteratorConcept<
                  _ForwardIterator>)
      __glibcxx_function_requires(_UnaryPredicateConcept<_Predicate,
        typename iterator_traits<_ForwardIterator>::value_type>)
      __glibcxx_requires_valid_range(__first, __last);

      __first = _GLIBCXX_STD_A::find_if(__first, __last, __pred);
      if(__first == __last)
        return __first;
      _ForwardIterator __result = __first;
      ++__first;
      for(; __first != __last; ++__first)
        if(!bool(__pred(*__first)))
          {
            *__result = _GLIBCXX_MOVE(*__first);
            ++__result;
          }
      return __result;
    }

很可能导致段错误的原因是该实现调用了_GLIBCXX_MOVE

5
“他们无法修改基础容器”并不正确。从操作结果来看,它们会修改容器,因为元素会被重新排列。但是它们不能使容器的大小减小(即在使用 std::remove_if 操作前后,container.size() 返回相同的值),只有一些元素(被操作删除的那些)是未指定的(根据 C++ 标准)。 - Nawaz
@Nawaz 我的措辞不太恰当。我已经重新表述了我的答案。 - Yuushi
2
C++标准算法不适用于容器,而适用于序列。容器是序列的一种来源,但并不是唯一的来源。 - Pete Becker
注意事项 - 使用 remove_iferase 搭配时要小心。如果你忘记了使用从向量中移除元素的方法,即 v.erase(remove_if(...), v.end()),而只是输入了 v.erase(remove_if(...)) - 这将仅删除第一个元素。在此之后,您将得到无效的 shared_ptr - Tomasz Gandor

7
迭代器可能是可以被解引用的,但共享指针可能不行。在对具有未指定值的共享指针进行解引用之前,应检查是否为空。

但是你认为数据发生了什么?如果实现如此简单。在他们的实现中使用移动语义是否会导致这种行为? - Martin Drozdik
3
我甚至没有考虑过数据发生了什么事。共享指针的值不是合同的一部分,因此我不关心它们。范围的成员可能会移动,导致空的共享指针,或者可能会进行“老式”的原地销毁和复制构造而不是赋值。您应该能够在系统上找到std::remove_if的源代码(因为它是一个模板),以验证这一点。 - CB Bailey

5

如果可能的话,C++11编译器将使用移动语义来移动那些不被std::remove_if“删除”的元素。移动一个shared_ptr会使原始的shared_ptr对象为空(它不再拥有指针)——在原始的shared_ptr上调用get()将返回一个空指针。

因此,如果您取消引用该shared_ptr,则会出现空指针取消引用。

因此,在总结一下,尽管迭代器仍然可取消引用,但shared_ptr可能无法这样做。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接