这是我在另一个项目中遇到的问题的简化。
假设我有以下类:
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructedn";
Instances().insert(this);
}
~MyClass() {
std::cout << "MyClass destructedn";
Instances().erase(this);
}
static std::unordered_set<MyClass*>& Instances() {
static std::unordered_set<MyClass*> _instances;
return _instances;
}
};
它有一个静态unordered_set
,用于跟踪类的现有实例。构造实例时,其地址将添加到集合中;当实例被销毁时,其地址将从集合中删除。
现在,我有另一个类,它的vector
为shared_ptr
包含MyClass
实例:
struct InstanceContainer {
std::vector<std::shared_ptr<MyClass>> instances;
};
这里的一个关键点是,在main
上面有一个此类的全局实例。这似乎是问题的一部分,因为在main
内部声明类不会产生问题。
在main
内部,我执行以下操作(假设InstanceContainer
的全局实例称为container
(:
container.instances.emplace_back(std::shared_ptr<MyClass>(new MyClass));
一切都很好,直到程序终止,当我在MyClass
的析构函数中执行Instances().erase(this)
时收到读取访问冲突("矢量下标超出范围"(。
我想也许我试图多次从_instances
中删除实例(因此cout
s(——但是,正如您所期望的那样,构造器只调用一次,析构函数只调用一次。我发现当这种情况发生时,_instances.size()
等于0
。奇怪的是,它等于在任何调用erase
之前0
。在从布景中删除任何东西之前,它是空的?!
在这一点上,我的理论是,这与程序终止时对象的破坏顺序有关。也许在调用MyClass
的析构函数之前释放了静态_instances
。
我希望有人能够对此有所了解,并确认是否正在发生的事情。
我现在的解决方法是在尝试擦除之前检查_instances.size()
是否0
。这安全吗?如果没有,我还能做什么?
如果重要,我正在使用 MSVC。下面是一个可执行示例。
这是发生的事情。首先构造类型InstanceContainer
的全局变量,然后再输入main
变量。函数静态变量_instances
稍后在首次调用Instances()
时创建。
在程序关闭时,这些对象的析构函数按构造的相反顺序调用。因此,首先销毁_instances
,然后InstanceContainer
,这反过来又破坏其共享指针向量,而共享指针向量又在仍在向量中的所有对象上运行~MyClass
,进而在已经销毁的_instances
上调用_instances.erase()
。因此,您的程序通过访问生存期已结束的对象来表现出未定义的行为。
有几种方法可以解决此问题。首先,您可以确保InstanceContainer::instances
在返回之前main
为空。不知道这有多可行,因为你从来没有解释过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
,因此稍后销毁。