假设我有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;
答案 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指出了标准要求的原因。
短:
长:
我将提供一些提示,为什么你根本不用担心,在这种情况下会调用析构函数。
考虑以下小班
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)
的规范(在序列容器要求表中)说明对该函数的要求是:
对于
vector
和deque
,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
和其他容器不是这种情况,因为它们不会将元素存储在vector
和deque
等连续的块中,因此删除单个元素只需要调整相邻元素之间的链接,并且不需要在块中“洗牌”其他元素以占据空位。
答案 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
。