今天,我们来看一下在尝试从中删除元素时发生的崩溃std::set。
进程正在退出,我们正在通知Contoso.dll。这促使 DLL 销毁其全局变量,其中一个显然是Gadget,或者至少它是一个全局变量,它的销毁会导致 的销毁Gadget。 的销毁Gadget反过来又释放Doodad,这导致它销毁,而 的调用的析构函数Doodad会DestroyWidget导致 的Widget销毁,这时我们注意到s_allWidgets已经被销毁了。
我们是静态初始化顺序惨败的受害者,但却是故事的另一端。
人们通常担心初始化方面的静态初始化顺序问题:确保初始化期间所依赖的对象本身已经初始化。但上行必有下行,您还必须确保销毁期间所依赖的对象本身尚未被销毁。
在这种情况下,作为清理的一部分被销毁,尽管仍有计划使用它
甚至在没有 DLL 的情况下也可能发生 投资者电子邮件列表 这种情况。具有静态存储持续时间的对象以与构造相反的顺序被破坏,因此我们可能会得到以下结果:
在程序启动时,我们构造mainGadget。Gadget构造函数不需要Widget立即进行,因此不存在静态初始化顺序混乱。稍后,我们决定主小工具需要一个窗口小部件,因此我们执行m_widget = std::make_unique<Widget>(),这将返回新构造的Widget,并在集合中注册窗口小部件。s_allWidgets
在析构时,首先析构,然后当主小工具析构时,它会析构现在非空的,这会析构并尝试将其从已析构的中取消注册。s_allWidgetsm_widgetWidgetstd::set
使用函数局部静态函数std::set在首次使用时按需创建 可避免静态初始化顺序混乱,但无助于解决静态析构顺序混乱。 是在之后std::set构造的,因此它先析构。Gadget
该特定组件使用了 Windows 实现库,因此它可以在关机期间跳过跟踪代码。 wil::ProcessShutdownInProgress()Widget
雷蒙德参与 Windows 的开发已有 30 多年。2003 年,他创办了一个名为 The Old New Thing 的网站,该网站的受欢迎程度远远超出了他的想象,这一发展至今仍让他感到毛骨悚然。该网站催生了一本书,巧合的是,这本书也名为 The Old New Thing(Addison Wesley 2007)。他偶尔会出现在 Windows Dev Docs Twitter 帐户上,讲述一些毫无用处的故事。
atexit 作为库的一部分,与全局变量的销毁有点不同(因为全局变量是语言本身的一部分)。
话虽如此,我 100% 赞成在破坏时以“尽力而为”的方式运行程序……只要每个人都知道这是尽力而为,而且不会太过冒险就行。(我之所以赞成这样做,是因为遥测、代码覆盖、缓冲区刷新、“验证是否可以安全地卸载 DLL/.so”等功能都可以从这种能力中受益,即使你永远无法做到……
阅读更多
登录进行投票或回复
BCS 6天前 0
解决此问题的另一种方法是使用函数静态变量来延迟构造全局对象,但将其放在堆上,使用原始指针,然后再也不用费心去破坏它。
只要静态仅在进程关闭期间超出范围(例如,没有 DLL 卸载/重新加载有趣的业务),则产生的内存泄漏基本上是无害的。 (另一方面,您仍然会失去破坏的任何其他副作用,因此最好只是由不再存在的进程正确清理的东西。)
登录进行投票或回复
凯文·诺里斯 2025 年 1 月 18 日 0
我更喜欢 Rust 的方法,其中静态变量永远不会被破坏,并且没有在进程终止时运行代码的方法。不幸的是,世界其他地方并不像 Rust 那样“合理”,因此他们不得不处理 Rust bug #126600 之类的有趣混乱,可以总结为“libc exit/atexit 很糟糕,并且建立在沙子的基础上。