std :: vector :: erase(迭代器位置)不一定会调用相应的元素的析构函数

时间:2013-07-05 15:14:00

标签: c++ stl

假设我有5个元素的std::vector V

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

STL vector实现将向上移动第4和第5个元素,然后销毁第5个元素。

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

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;

6 个答案:

答案 0 :(得分:4)

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

  
    

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

  

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

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

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

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

此时,实现仍需要将vector大小减小1并销毁第5个元素,但正如我们所见,不允许其他destrucor调用。 (另请注意,移动分配操作员不会按标准要求调用两次。)QED。

答案 1 :(得分:3)

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

短:

std :: vector :: erase(迭代器位置)不一定会调用相应元素的析构函数”[Op;标题] 但是调用析构函数,处理已转移到另一个对象的相应元素的数据(通过移动构造函数移动到移动到或通过RAII移动到临时实例)

长:

为什么你不必依赖 ith 析构函数来调用它。

我将提供一些提示,为什么你根本不用担心,在这种情况下会调用析构函数。

考虑以下小班

  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(它可能仍然是成员函数;但是测试只有一个指针......)。 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.

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

如果正确设计了赋值操作,那么依赖于对象内部状态的每个析构函数都将使用适当的对象进行调用。

答案 2 :(得分:2)

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

答案 3 :(得分:2)

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

  

对于vectordequeT应为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和其他容器不是这种情况,因为它们不会将元素存储在vectordeque等连续的块中,因此删除单个元素只需要调整相邻元素之间的链接,并且不需要在块中“洗牌”其他元素以占据空位。

答案 4 :(得分:2)

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

#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)物体的某些基本部分可以是   在对象失去其身份的情况下逐个替换。界定   随着时间的推移,我们引入了必要部分和概念的概念   基本形式。

     

定义:对象的一个​​重要部分是一个不可或缺的部分,如果它被删除,对象就失去了它的身份,因此它   消失。

答案 5 :(得分: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