从集合中删除元素时下标超出范围

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,其中 vectorshared_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 中删除实例(因此 couts)——但是,构造函数只被调用一次,而析构函数只被调用如您所料,被调用一次。我发现发生这种情况时,_instances.size() 等于 0。奇怪的是,在任何调用 erase 之前它等于 0。在从集合中删除任何东西之前,它是空的?!

我目前的理论是,这与程序终止时对象被销毁的顺序有关。也许静态 _instances 在调用 MyClass 的析构函数之前被释放。

我希望有人能够阐明这一点,并确认这是否是正在发生的事情。

我现在的解决方法是在尝试擦除之前检查 _instances.size() 是否为 0。这样安全吗?如果没有,我还能做什么?

如果重要的话,我正在使用 MSVC。这是一个 executable example.

事情是这样的。在输入 main 之前,首先构造 InstanceContainer 类型的全局变量。函数静态变量 _instances 稍后在第一次调用 Instances() 时创建。

在程序关闭时,这些对象的析构函数以相反的构造顺序被调用。因此,_instances 首先被销毁,然后是 InstanceContainer,这反过来又销毁了它的共享指针向量,这又对仍在向量中的所有对象 运行 ~MyClass,依次调用 _instances.erase() 已经被销毁的 _instances。因此,您的程序通过访问生命周期已结束的对象而表现出未定义的行为。

您可以通过多种方式解决此问题。第一,您可以确保 InstanceContainer::instancesmain 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 较早创建,因此较晚销毁。