从全局对象的构造函数调用std::atexit的顺序

7

cppreference关于std::atexit的说明:

这些函数可以与具有静态存储期的对象的销毁和彼此同时调用,保证如果A的注册在B之前,则对B的调用在对A的调用之前被排序,静态对象构造函数和对atexit的调用之间的排序也适用于此。

我理解这段话的意思是,如果在静态初始化期间调用std::atexit,则在注册该函数时调用的std::atexit所初始化的最后一个静态对象的销毁之前调用已注册函数。我也将“可能并发调用”解释为调用可以发生在静态对象销毁之间,而不是多线程解释的意思。

我想知道的是,在这个顺序的上下文中,对象何时被认为已初始化(即开始初始化还是完成初始化)。我编写了一个简短的测试来测试这一点:

#include <cstdlib>
#include <iostream>

struct foo
{
    foo() 
    {
        std::cout << "ctor\n";
        std::atexit([]() { std::cout << "atexit\n"; });
    }
    ~foo()
    {
        std::cout << "dtor\n";
    }
};

foo my_foo;

int main()
{
    return 0;
}

我得到的输出结果是 (http://cpp.sh/3bllu) :
ctor
dtor
atexit

这让我相信,在我的构造函数完成之前,my_foo 在这个上下文中并未被视为已初始化。换句话说,该函数在 my_foo 初始化之前被视为已注册,因此注册的函数会在my_foo销毁之后执行。
我似乎找不到任何可以保证这种行为的东西,而且我甚至不确定我对引用段落的最初解释是否正确。我描述的行为是我可以依赖的内容,还是实现定义或未定义的行为?

1
通常情况下,对象初始化在构造函数完成时被认为是完整的。当异常在构造函数中抛出时,请查看析构函数在异常处理期间如何处理,这是另一个例子。 - user4442671
2
@JesperJuhl 真的吗?再见了,std::cout... - SergeyA
2
@JesperJuhl,不,我没有。你确实说过“所有全局变量,包括单例模式”。例如,我无法想象一个不使用全局变量或单例模式的日志记录器的复杂应用程序。 - SergeyA
3
这个对话(争论)如何有助于原始问题的建设性或实用性? - ricco19
1
@JesperJuhl 出于好奇,您是否也拒绝使用 static const 成员?在静态初始化的上下文中,它们本质上与全局变量相同。无论如何,消除所有全局变量并不总是像听起来那么容易,特别是考虑到遗留代码和依赖关系。有时候,为了避免全局变量而进行的花式操作并不值得投入人力或增加代码复杂性。 - François Andrieux
显示剩余11条评论
1个回答

7
调用析构函数将在调用传递给 atexit 函数的函数之前发生。从 [basic.start.term],第5段:

如果对 std::atexit 的调用强制先于具有静态存储持续期的对象的初始化完成,则对于该对象的析构函数的调用先于传递给 std::atexit 的函数的调用。


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