6

假设我有std::vector V5 个元素,

V.erase(V.begin() + 2)删除第三个元素。

STLvector实现将向上移动第 4 和第 5 个元素,然后破坏第 5 个元素。

即擦除a中的元素ivector并不能保证调用i析构函数。对于std::list,情况并非如此。擦除第 i 个元素会调用第 i 个元素的析构函数。

STL 对这种行为有什么看法?

这是取自我系统的 stl_vector.h 的代码:

392   iterator erase(iterator __position) {
393     if (__position + 1 != end())
394       copy(__position + 1, _M_finish, __position);
395     --_M_finish;
396     destroy(_M_finish);
397     return __position;
4

6 回答 6

4

C++11 标准 23.3.6.5/4 说(重点是我的):

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

如果实现在第三个元素上调用了析构函数,它将不符合要求。

实际上,假设在第三个元素上调用了析构函数。由于只擦除了一个元素,因此无法再次调用析构函数。

在析构函数调用之后,第三个位置包含原始内存(不是完全构造的对象T)。因此,实现需要调用移动构造函数来从第 4 个位置移动到第 3 个位置。

它不能破坏第 4 个元素(因为它不能再调用析构函数),然后要从第 5 个元素移动到第 4 个元素,它必须调用移动赋值运算符。

此时,实现仍需要将vector大小减 1 并销毁第 5 个元素,但正如我们所见,不允许调用其他析构函数。(另请注意,移动赋值运算符不会按照标准的要求被调用两次。) QED。

于 2013-07-05T15:39:41.257 回答
3

这是完全有效的行为。@Cassio Neri 指出了为什么标准要求它。

短的:

" std::vector::erase(iterator position) 不一定调用相应元素的析构函数" [Op; Headline]但是调用了析构函数,处理已传输到另一个对象的相应元素的数据(通过移动构造函数到移动对象通过 RAII 到临时实例)。

长:

为什么你不必依赖第 i 个析构函数来调用。

我将提供一些提示,为什么您根本不应该担心,在这种情况下调用了哪个析构函数。

考虑以下小班

  class test
  {
    int * p;
  public:
    test (void) : p(new int[5]) { cout << "Memory " << p << " claimed." << endl;  }
    ~test (void) { cout << "Memory " << p << " will be deleted." << endl; delete p;  }
  };

如果您正确处理对象移动分配,则无需担心正确调用了析构函数这一事实。

    test& operator= (test && rhs)
    { 
      cout << "Move assignment from " << rhs.p << endl;
      std::swap(p, rhs.p);
      return *this;
    }

您的移动赋值运算符必须将“覆盖”的对象的状态转移到“移出”的对象(rhs此处),因此它的析构函数将采取适当的行动(如果析构函数需要处理某些事情)。也许您应该使用“交换”成员函数之类的东西来为您进行转移。

如果您的对象是不可移动的,则在将新数据复制到对象之前,您必须在复制分配操作中处理已擦除对象的“清理”(或依赖于对象当前状态的任何操作)。

    test& operator= (test const &rhs)
    {
      test tmp(rhs);
      std::swap(p, tmp.p);
      return *this;
    }

在这里,我们使用 RAII 并再次使用swap(它也可能仍然是一个成员函数;但 test 只有一个指针......)。的析构函数tmp将使事情变得舒适。

让我们做一个小测试:

  #include <vector>
  #include <iostream>
  using namespace std;
  class test
  {
    int * p;
  public:
    test (void) : p(new int[5]) { cout << "Memory " << p << " claimed." << endl;  }
    test& operator= (test && rhs)
    { 
      cout << "Move assignment from " << rhs.p << endl;
      std::swap(p, rhs.p);
      return *this;
    }
    ~test (void) { cout << "Memory " << p << " will be deleted." << endl; delete p;  }
  };

  int main (void)
  {
    cout << "Construct" << endl;
    std::vector<test> v(5);
    cout << "Erase" << endl;
    v.erase(v.begin()+2);
    cout << "Kick-off" << endl;
    return 0;
  }

结果是

Construct
Memory 012C9F18 claimed.
Memory 012CA0F0 claimed.
Memory 012CA2B0 claimed. // 2nd element
Memory 012CA2F0 claimed.
Memory 012CA110 claimed.
Erase
Move assignment from 012CA2F0
Move assignment from 012CA110
Memory 012CA2B0 will be deleted. // destruction of the data of 2nd element
Kick-off
Memory 012C9F18 will be deleted.
Memory 012CA0F0 will be deleted.
Memory 012CA2F0 will be deleted.
Memory 012CA110 will be deleted.

如果您的移动(或复制)分配操作将关键属性移交给将要销毁的对象,则声明的每个内存位置都将被正确释放。

如果你的赋值操作设计得当,每个依赖对象内部状态的析构函数都会被调用,并带有适当的对象。

于 2013-07-05T16:13:59.930 回答
3

该标准说这是预期的,vector::erase(const_iterator)(在序列容器要求表中)的规范说对该功能的要求是:

vectordeque,T应为MoveAssignable

要求的原因MoveAssignable是以下每个元素将被(移动)分配到它们之​​前的元素上,并且最后一个元素被销毁。

从理论上讲,原始 STL 可能会以不同的方式完成它并按照您的预期破坏已擦除的元素,但有充分的理由没有选择。如果您销毁已擦除的元素,则会在向量中留下一个“洞”,这不是一个选项(向量必须记住洞在哪里,如果用户说v[5]向量必须记住那里有一个洞并返回v[6]相反。)所以有必要“洗牌”后面的元素来填补这个洞。这可以通过在原地销毁第 N 个元素(即v[N].~value_type())然后使用放置new在该位置创建一个新对象(即::new ((void*)&v[N]) value_type(std::move(v[N+1]))) 然后对后面的每个元素执行相同的操作,直到到达最后,但是在许多情况下这会导致性能更差。如果现有元素已分配内存,例如容器本身,则分配给它们可能允许它们重用该内存,但销毁它们然后构造新元素将需要重新分配和重新分配内存,这可能会慢得多并且可能使堆碎片化. 因此,我们有一个很好的理由来分配改变元素的值,而不必改变它们的身份。

和其他容器不是这种情况,std::list因为它们不会将元素存储在像vectorand这样的连续块中deque,因此删除单个元素只涉及调整相邻元素之间的链接,并且无需“洗牌”其他元素块占据空位。

于 2013-07-05T15:40:54.733 回答
2

参考 Mats Petersson 的例子,也许这个例子会更清楚地表明 destroy 2 确实发生了,我们只是没有可用于内置类型的析构函数,我们可以方便地添加打印输出语句:

#include <vector>
#include <iostream>
#include <utility>

using namespace std;

struct Integer
{
    int x;
    Integer(int v) : x(v) {}
    ~Integer() { cout << "Destroy Integer=" << x << endl; }
};

class X
{
    Integer Int;
public: 
    X(int v) : Int(v) {}
    X operator=(const X& a) 
    { 
        auto tmp(a.Int);
        swap(this->Int, tmp);
        cout << "copy x=" << Int.x << endl;
        return *this; 
    }
};

int main()
{
    vector<X> v;
    for(int i = 0; i < 5; i++)
    {
        X a(i); 
        v.push_back(a);
    }
    cout << "Erasing ... " << endl;
    v.erase(v.begin() + 2);
}

这将打印:

Destroy Integer=0
Destroy Integer=0
Destroy Integer=1
Destroy Integer=0
Destroy Integer=1
Destroy Integer=2
Destroy Integer=0
Destroy Integer=1
Destroy Integer=2
Destroy Integer=3
Destroy Integer=0
Destroy Integer=1
Destroy Integer=2
Destroy Integer=3
Destroy Integer=4
Erasing ...
Destroy Integer=2
copy x=3
Destroy Integer=2
Destroy Integer=3
Destroy Integer=3
copy x=4
Destroy Integer=3
Destroy Integer=4
Destroy Integer=4

(在程序退出时跳过整个向量的析构函数调用的打印输出)

看待这个问题的一种方法是问自己:从向量中擦除对象是什么意思?这意味着,给定一种识别该对象的方法,您将无法在擦除后在向量中找到它。也许它是一个被覆盖的值,从而获得了一个新的身份。如果它拥有可以识别它的资源,那么正如其他人所提到的,只要移动、分配和复制做正确的事情,这些资源就会被适当地释放。此外,向量的大小将反映少一个对象。

为了您的哲学乐趣,以下是 Stepanov(主要 STL 作者)的一些注释:

对象的组成部分是实现其主要目的所需的对象的那些部分。组成部分之间的联系构成了物体的整体形式。我们对基本部分的定义有两个直观的限制:(i)对于某些对象,可以将它们分开,这将导致它们失去它们的身份,然后它们可以组合在一起,这意味着它们将重新获得它们的身份。这允许对象存在、消失并随后重新出现;因此它们的存在是不连续的。(ii) 一个物体的一些重要部分可以被一个一个地替换,而不会失去它的身份。为了定义跨时间的身份,我们引入了基本部分和基本形式的概念。

Definition: An essential part of an object is an integral part such that if it is removed, the object loses its identity, hence it disappears.

于 2013-07-05T16:43:59.960 回答
2

与 不同std::liststd::vector它的元素是连续的。因此,当从容器中间删除一个元素时,复制分配所有需要移动的元素会更有意义。在这种情况下,将调用最后一个移位元素的析构函数。这避免了向量的整个数据的重新分配。

于 2013-07-05T15:30:08.337 回答
0

这是一个显示问题的小程序,是的,如果您依赖于为该对象调用的析构函数,则需要执行此代码之外的其他操作:

#include <iostream>
#include <vector>

using namespace std;

class X
{
    int x;
public: 
    X(int v) : x(v) {}
    ~X() { cout << "Destroy v=" << x << endl; }
    X operator=(const X& a) { x = a.x; cout << "copy x=" << x << endl; return *this; }

};

int main()
{
    vector<X> v;
    for(int i = 0; i < 5; i++)
    {
    X a(i); 
    v.push_back(a);
    }
    cout << "Erasing ... " << endl;
    v.erase(v.begin() + 2);
}

输出是:

Destroy v=0
Destroy v=0
Destroy v=1
Destroy v=0
Destroy v=1
Destroy v=2
Destroy v=3
Destroy v=0
Destroy v=1
Destroy v=2
Destroy v=3
Destroy v=4
Erasing ... 
copy x=3
Destroy v=3
copy x=4
Destroy v=4     <<< We expedct "destroy 2", not "destroy 4". 
Destroy v=4
Destroy v=0
Destroy v=1
Destroy v=3
Destroy v=4

解决此问题的一种变体是存储(智能)指针,然后手动复制指针然后复制delete它。

于 2013-07-05T15:33:30.013 回答