3

背景

不久前,我遇到了一些我发现非常奇怪且看似不正确的行为,我向 GCC 提交了一份错误报告。您可以在此处查看报告和回复:

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

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

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

我的问题

在我的 Mac(当前为 OS X,Darwin 12.2.0 x86_64)附带的 C++ stdlib 发行版中,第 106-116 行的实现std::vector::erase()如下/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;
  }

请注意,destroy()将在调用 this 之前为向量中最后一个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;
}

当我在我的 Mac 上使用 g++ 版本 4.2.1(无命令行参数)编译它时,它会在我运行它时产生以下结果:

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

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

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

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

4

4 回答 4

4

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

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

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

于 2012-10-19T21:32:11.590 回答
3

实际上,没有问题。在行

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

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

在 C++11 中,您可能希望使用移动而不是复制;但你发布的是一个好的 C++03 实现std::vector::erase

于 2012-10-19T21:32:24.577 回答
2

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

于 2012-10-19T21:33:45.467 回答
2

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

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

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

  • move-assign 从 4 到 3,然后从 5 到 4,然后销毁 5。

事实上,vector::erase第一种方式在 23.2.4.3/4 中被 C++03 标准禁止:

复杂度:T的析构函数被称为等于被擦除元素个数的次数,而T的赋值运算符被称为等于被擦除元素之后向量中元素个数的次数。

尽管此文本主要旨在说明操作的运行时复杂性,但您会看到它要求第二个实现。C++11 用“移动赋值”代替“赋值”说了同样的话。

第一种方法还有一个更根本的问题,一般来说(虽然不是为了int,因此也不是为了MyClass),复制可能会失败。如果erase破坏了向量的第三个元素,然后从第四个元素复制失败,那么向量将处于相当危险的状态——第三个元素不再是一个合适的对象。因此,标准中的限制不仅仅是定义运行时,它还可以防止这种糟糕的失败情况。

于 2012-10-19T21:49:58.190 回答