应该std :: vector :: erase()破坏被删除的元素吗? (而不是最后一个元素)

时间:2012-10-19 21:24:49

标签: c++ stdvector

背景

前段时间,我遇到了一些我发现非常奇怪且看似不正确的行为,我向GCC提交了一份关于它的错误报告。你可以看到我在这里得到的报告和回复:

http://gcc.gnu.org/bugzilla/show_bug.cgi?id=47305

(我将在这里复制大部分内容。)

当时,我不理解答案,但不是StackOverflow的成员,也没有人问过它,所以我只是破解了一个解决方法并继续。但最近,我正在重新审视这段代码,我仍然不明白这不是一个错误的理由,所以......

我的问题

在我的Mac附带的C ++ stdlib发行版中(目前是OS X,Darwin 12.2.0 x86_64),std::vector::erase()行106-116中/usr/include/c++/4.2.1/vector.tcc的实现如下所示:

template<typename _Tp, typename _Alloc>
  typename vector<_Tp, _Alloc>::iterator
  vector<_Tp, _Alloc>::
  erase(iterator __position)
  {
    if (__position + 1 != end())
      std::copy(__position + 1, end(), __position);
    --this->_M_impl._M_finish;
    this->_M_impl.destroy(this->_M_impl._M_finish);
    return __position;
  }

请注意,将为 last 中的元素调用destroy() 在调用此erase()之前的向量,而不是被调用 __position指向的元素。我认为这是不正确的 - 我认为 应该为destroy()指向的元素调用__position。对于 简单的POD类型,这不是一个很大的交易,但对于那些类 析构函数有副作用(比如智能指针),它可能很关键。

以下代码说明了问题:

#include <vector>
#include <iostream>

class MyClass
{
    int m_x;
public:
     MyClass(int x) : m_x(x) { }
    ~MyClass()
    {
        std::cerr << "Destroying with m_x=" << m_x << std::endl;
    }
};

int main(void)
{
    std::vector<MyClass> testvect;
    testvect.reserve(8);
    testvect.push_back(MyClass(1));
    testvect.push_back(MyClass(2));
    testvect.push_back(MyClass(3));
    testvect.push_back(MyClass(4));
    testvect.push_back(MyClass(5));

    std::cerr << "ABOUT TO DELETE #3:" << std::endl;

    testvect.erase(testvect.begin() + 2);

    std::cerr << "DONE WITH DELETE." << std::endl;

    return 0;
}

当我使用g ++版本4.2.1(没有命令行参数)编译它时 Mac,它在我运行时产生以下内容:

Destroying with m_x=1
Destroying with m_x=2
Destroying with m_x=3
Destroying with m_x=4
Destroying with m_x=5
ABOUT TO DELETE #3:
Destroying with m_x=5
DONE WITH DELETE.
Destroying with m_x=1
Destroying with m_x=2
Destroying with m_x=4
Destroying with m_x=5

请注意,“关于删除#3”消息后的关键行显示 析构函数实际上被称为(我的副本)我添加的第五件事。 重要的是, #3的析构函数永远不会被调用!!

看起来erase()的版本也需要一个范围(两个迭代器) 有一个类似的问题。

所以我的问题是,我错误地认为我从矢量中删除的元素的析构函数被调用了吗? 看来如果你不能依靠这个,你无法安全地在向量中使用智能指针。或者这只是Apple发布的STL矢量实现中的一个错误?我错过了一些明显的东西吗?

4 个答案:

答案 0 :(得分:4)

当您erase包含 3 的元素时,必须将以下元素移回以填充空白。然后,元素#3 被分配#4 具有的内容,#4 被分配#5 具有的内容。最后一个元素#5 会留下它所拥有的任何值,因为它无论如何都要被删除。

vector超出范围时,您会看到剩余的4个元素被销毁。

如果您要在vector中保留智能指针,则在调用赋值运算符时将正确释放资源。

答案 1 :(得分:3)

实际上,没有问题。在行

std::copy(__position + 1, end(), __position);

删除的元素被连续的元素覆盖;如果它拥有需要释放的资源,它将在operator=

中执行

在C ++ 11中,您可能希望使用move而不是copy;但你发布的是std::vector::erase的OK C ++ 03实现。

答案 2 :(得分:2)

析构函数只会被调用最后一个元素,但被擦除的对象会通过从下一个元素分配来覆盖它。因此赋值运算符释放旧资源。当类型是智能指针时,这意味着要调整引用,并在适当的情况下删除受控对象。

答案 3 :(得分:2)

这是一个合理的观点,至少有两种不同的方式可以考虑实施erase

  • 销毁元素3,然后从4复制构造元素3,然后从5复制构造元素3,然后销毁5。
  • 从4复制分配到3,然后从5分配到4,然后销毁5。

C ++ 11引入了第三种方法:

  • 从4分别移动到3,然后从5分配到4,然后消灭5。

事实上,对于vector::erase,第一种方式是由23.2.4.3/4中的C ++ 03标准禁止的:

  

复杂性:T的析构函数被称为等于的次数   擦除元素的数量,但T的赋值运算符是   调用的次数等于中的元素数   擦除元素后的矢量。

尽管本文主要用于指示操作的运行时复杂性,但您会看到它要求第二次实现。 C ++ 11用“移动分配”代替“赋值”说了同样的事情。

第一种方式还存在一个更基本的问题,即一般情况下(虽然不适用于int,因此也不适用于MyClass),复制可能会失败。如果erase破坏了向量的第三个元素,然后第4个元素的副本失败,则向量将处于相当危险的状态 - 第三个元素不再是正确的对象。因此,标准中的限制不仅仅是定义运行时,还可以防止这种错误的故障情况。