从集合中删除元素时下标超出范围
Subscript out of range when removing element from set
这是我在另一个项目中遇到的问题的简化。
假设我有以下 class:
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructed\n";
Instances().insert(this);
}
~MyClass() {
std::cout << "MyClass destructed\n";
Instances().erase(this);
}
static std::unordered_set<MyClass*>& Instances() {
static std::unordered_set<MyClass*> _instances;
return _instances;
}
};
它有一个静态 unordered_set
,用于跟踪 class 的现有实例。构造实例时,将其地址添加到集合中;当一个实例被销毁时,它的地址将从集合中删除。
现在,我有另一个 class,其中 vector
个 shared_ptr
包含 MyClass
的实例:
struct InstanceContainer {
std::vector<std::shared_ptr<MyClass>> instances;
};
这里的一个关键点是main
上面有一个这个class的全局实例。这似乎是问题的一部分,因为在 main
中声明 class 不会产生问题。
在 main
内部,我执行以下操作(假设 InstanceContainer
的全局实例称为 container
):
container.instances.emplace_back(std::shared_ptr<MyClass>(new MyClass));
程序终止之前一切正常,当我在 MyClass
的析构函数中执行 Instances().erase(this)
时遇到读取访问冲突 ("vector subscript out of range")。
我想也许我试图多次从 _instances
中删除实例(因此 cout
s)——但是,构造函数只被调用一次,而析构函数只被调用如您所料,被调用一次。我发现发生这种情况时,_instances.size()
等于 0
。奇怪的是,在任何调用 erase
之前它等于 0
。在从集合中删除任何东西之前,它是空的?!
我目前的理论是,这与程序终止时对象被销毁的顺序有关。也许静态 _instances
在调用 MyClass
的析构函数之前被释放。
我希望有人能够阐明这一点,并确认这是否是正在发生的事情。
我现在的解决方法是在尝试擦除之前检查 _instances.size()
是否为 0
。这样安全吗?如果没有,我还能做什么?
如果重要的话,我正在使用 MSVC。这是一个 executable example.
事情是这样的。在输入 main
之前,首先构造 InstanceContainer
类型的全局变量。函数静态变量 _instances
稍后在第一次调用 Instances()
时创建。
在程序关闭时,这些对象的析构函数以相反的构造顺序被调用。因此,_instances
首先被销毁,然后是 InstanceContainer
,这反过来又销毁了它的共享指针向量,这又对仍在向量中的所有对象 运行 ~MyClass
,依次调用 _instances.erase()
已经被销毁的 _instances
。因此,您的程序通过访问生命周期已结束的对象而表现出未定义的行为。
您可以通过多种方式解决此问题。第一,您可以确保 InstanceContainer::instances
在 main
returns 之前为空。不知道这有多可行,因为您从未解释过 InstanceContainer
在您的设计中扮演什么角色。
第二,你可以在堆上分配 _instances
,然后泄漏它:
static std::unordered_set<MyClass*>& Instances() {
static auto* _instances = new std::unordered_set<MyClass*>;
return *_instances;
}
这将通过销毁全局对象使其保持活力。
三,你可以在InstanceContainer
全局变量的定义之前加上这样的东西:
static int dummy = (MyClass::Instances(), 0);
这将确保 _instances
较早创建,因此较晚销毁。
这是我在另一个项目中遇到的问题的简化。
假设我有以下 class:
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructed\n";
Instances().insert(this);
}
~MyClass() {
std::cout << "MyClass destructed\n";
Instances().erase(this);
}
static std::unordered_set<MyClass*>& Instances() {
static std::unordered_set<MyClass*> _instances;
return _instances;
}
};
它有一个静态 unordered_set
,用于跟踪 class 的现有实例。构造实例时,将其地址添加到集合中;当一个实例被销毁时,它的地址将从集合中删除。
现在,我有另一个 class,其中 vector
个 shared_ptr
包含 MyClass
的实例:
struct InstanceContainer {
std::vector<std::shared_ptr<MyClass>> instances;
};
这里的一个关键点是main
上面有一个这个class的全局实例。这似乎是问题的一部分,因为在 main
中声明 class 不会产生问题。
在 main
内部,我执行以下操作(假设 InstanceContainer
的全局实例称为 container
):
container.instances.emplace_back(std::shared_ptr<MyClass>(new MyClass));
程序终止之前一切正常,当我在 MyClass
的析构函数中执行 Instances().erase(this)
时遇到读取访问冲突 ("vector subscript out of range")。
我想也许我试图多次从 _instances
中删除实例(因此 cout
s)——但是,构造函数只被调用一次,而析构函数只被调用如您所料,被调用一次。我发现发生这种情况时,_instances.size()
等于 0
。奇怪的是,在任何调用 erase
之前它等于 0
。在从集合中删除任何东西之前,它是空的?!
我目前的理论是,这与程序终止时对象被销毁的顺序有关。也许静态 _instances
在调用 MyClass
的析构函数之前被释放。
我希望有人能够阐明这一点,并确认这是否是正在发生的事情。
我现在的解决方法是在尝试擦除之前检查 _instances.size()
是否为 0
。这样安全吗?如果没有,我还能做什么?
如果重要的话,我正在使用 MSVC。这是一个 executable example.
事情是这样的。在输入 main
之前,首先构造 InstanceContainer
类型的全局变量。函数静态变量 _instances
稍后在第一次调用 Instances()
时创建。
在程序关闭时,这些对象的析构函数以相反的构造顺序被调用。因此,_instances
首先被销毁,然后是 InstanceContainer
,这反过来又销毁了它的共享指针向量,这又对仍在向量中的所有对象 运行 ~MyClass
,依次调用 _instances.erase()
已经被销毁的 _instances
。因此,您的程序通过访问生命周期已结束的对象而表现出未定义的行为。
您可以通过多种方式解决此问题。第一,您可以确保 InstanceContainer::instances
在 main
returns 之前为空。不知道这有多可行,因为您从未解释过 InstanceContainer
在您的设计中扮演什么角色。
第二,你可以在堆上分配 _instances
,然后泄漏它:
static std::unordered_set<MyClass*>& Instances() {
static auto* _instances = new std::unordered_set<MyClass*>;
return *_instances;
}
这将通过销毁全局对象使其保持活力。
三,你可以在InstanceContainer
全局变量的定义之前加上这样的东西:
static int dummy = (MyClass::Instances(), 0);
这将确保 _instances
较早创建,因此较晚销毁。