引用一个可能已被销毁的静态对象

7
假设我有以下代码。

Something.hpp

#pragma once

class Something {
public:
    static Something& get();
private: 
    Something();
};

Something.cpp

#include "Something.hpp"
#include <iostream>
using namespace std;

Something& Something::get() {
    static Something something;
    return something;
}
Something::Something() {
    cout << "Something()" << endl;
}

main.cpp

#include <iostream>
using namespace std;

struct SomethingElse {
    ~SomethingElse() {
        Something::get();
        cout << "~SomethingElse" << endl; 
    }
};

void func() {
    static SomethingElse something_else;
    // do something with something_else
}

int main() {
    func();
    return 0;
}

是否可以创建多个Something对象的实例?标准是否有关于静态对象销毁序列化的规定?

注意:我知道文件级静态变量在不同翻译单元之间销毁是未定义的,我想知道函数作用域静态变量的情况(其中包含C++运行时中的双重检查锁定模式)。对于同一翻译单元的文件级静态变量,编译器可以通过代码中变量的布局(静态)来确保构造和销毁的序列化,但当变量在调用函数时动态地懒惰创建时会发生什么呢?

注意:原始变量呢?我们可以期望它们保留值直到程序结束吗?因为它们不需要被销毁。


编辑

在cppreference.com上找到了这个(http://en.cppreference.com/w/cpp/utility/program/exit

如果线程本地或静态对象A的构造函数或动态初始化在线程本地或静态对象B之前被序列化,则B的销毁完成在A的销毁开始之前被序列化

如果这是真的,那么每个静态对象的销毁都是串行的吗?但我也发现了这个

https://isocpp.org/wiki/faq/ctors#construct-on-first-use-v2,与标准相矛盾


1
共享指针 - David Haim
1
我在标准中找不到关于这种情况的任何信息 - 在终止期间初始化静态对象 - 。我建议保险起见假设它应该是未定义的。 - molbdnilo
@molbdnilo也许这个问题应该发在别处吗?我不确定该去哪里。 - Curious
2个回答

4
在C++规范中,静态(和全局非静态)对象的构造和销毁顺序对于单个翻译单位是明确定义的!
如果有多个翻译单位(多个源文件),则TUs之间的构造/销毁顺序未定义。
因此,您展示的代码可能具有未定义的行为。

请注意,如果讨论超过15条评论,请使用聊天室。评论已存档在此处:http://chat.stackoverflow.com/rooms/132635/discussion-on-answer-by-some-programmer-dude-referencing-a-possibly-destroyed-st。 - Bhargav Rao

4

[stmt.dcl] ¶4

在控制流第一次通过块作用域具有静态存储期或线程存储期的变量的声明时,进行其动态初始化。

[basic.start.term] ¶1

如果具有静态存储期的对象的构造函数或动态初始化在另一个对象之前完成,则第二个对象的析构函数的完成在第一个对象的析构函数的启动之前。

¶2

如果函数包含已被销毁的具有静态或线程存储期的块作用域对象,并且在具有静态或线程存储期的对象的销毁期间调用该函数,则如果控制流穿过先前销毁的块作用域对象的定义,则程序具有未定义行为

我们在SomethingElse的析构函数中冒着引发此未定义行为的风险:

SomethingElse::~SomethingElse() {
    Something::get();
}

如果存在一个具有静态存储期的SomethingElse实例,则有四种可能性:
  1. Something的单个实例在SomethingElse之前被构造。它的销毁将发生在SomethingElse之后,因此行为是良好定义的。

  2. Something的单个实例在SomethingElse之后被构造。它的销毁将在SomethingElse之前发生,因此行为是未定义的,如上所述。

  3. Something的单个实例在没有与SomethingElse的构造同步的情况下在另一个线程中被构造。销毁可能同时发生,因此行为是未定义的。

  4. 还没有构造Something的实例(即第一次调用Something::get)。在这种情况下,程序在SomethingElse之后调用了Something的构造,这意味着Something的销毁必须在SomethingElse之前发生,但由于SomethingElse的销毁已经开始,这是一个矛盾,因此行为是未定义的。(技术上讲,“顺序之前”关系中存在一个循环。)


这是否意味着跨翻译单元的构造和销毁是明确定义的? - Curious
对于块作用域的静态变量,是的。对于命名空间作用域的静态变量(在这里不相关),这是未指定的,除非保证对象的销毁顺序与它们的初始化顺序相反。在线程启动后第一次odr使用的对象可能会同时初始化。 - Oktalist
你确定吗?SO 上到处都有很多答案认为,在函数作用域中的静态变量的销毁顺序在不同的翻译单元之间是未指定的。 - Curious
@Curious 这个答案 支持我的观点(翻译单元在评论中有讨论)。我没有找到任何不同意的。 - Oktalist
请查看https://isocpp.org/wiki/faq/ctors#construct-on-first-use-v2,它有不同的观点。我不知道为什么会存在这样的歧义。 - Curious
@Curious,这个常见问题解答支持我的说法:“如果abc的构造函数使用了ans,你通常应该没问题,因为在静态反初始化期间,运行时系统会在最后一个对象被销毁后销毁ans。” - Oktalist

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接