在全局变量的析构函数中初始化 thread_local 变量是否合法?

15

这个程序:

#include <iostream>

struct Foo {
    Foo() {
        std::cout << "Foo()\n";
    }

    ~Foo() {
        std::cout << "~Foo()\n";
    }
};

struct Bar {
    Bar() {
        std::cout << "Bar()\n";
    }

    ~Bar() {
        std::cout << "~Bar()\n";
        thread_local Foo foo;
    }
};

Bar bar;

int main() {
    return 0;
}

打印

Bar()
~Bar()
Foo()

针对我的情况(GCC 6.1,Linux,x86-64),~Foo()从未被调用。这是否是预期行为?


3
我尝试着实现libc++abi的一部分(特别是__cxa_thread_atexit()),想知道是否需要处理这种情况。 - Tavian Barnes
5
很可能只是因为 coutfoo 之前被销毁了。尝试从 Foo 的析构函数中抛出异常,看看是否会调用 std::terminate - Baum mit Augen
1
@BaummitAugen 请查看http://en.cppreference.com/w/cpp/io/ios_base/Init,了解C++如何解决`std::c{in,out,err}`的初始化顺序混乱问题。 - Tavian Barnes
@KerrekSB,看起来你的melpon链接停止产生初始结果了。如果现在重新运行它,它会得到与clang相同的结果:~Foo没有被调用(也许正如Tavian所说,它是libc/C++ std lib的一个函数,而不是主要的编译器)。 - Brian Cain
1
@BrianCain:是的,这与当前语言演变一致,旨在将所有线程本地对象视为在静态对象之前被销毁,以便线程本地析构函数可以依赖于静态对象的存在。 - Kerrek SB
显示剩余11条评论
2个回答

9
标准不覆盖这种情况;最严格的解释是,在具有静态存储期的对象的析构函数中初始化 thread_local 是合法的,但允许程序继续正常完成则是非法的。
问题出在 [basic.start.term]

1 - 具有静态存储期的已初始化对象(即其生命周期已经开始)的析构函数([class.dtor])会在 main 函数返回和调用 std::exit ([support.start.term]) 时被调用。给定线程内具有线程存储期的已初始化对象的析构函数将在该线程的初始函数返回和该线程调用 std::exit 时被调用。该线程内所有具有线程存储期的已初始化对象的析构函数的完成在任何具有静态存储期的对象的析构函数启动之前进行序列化。[...]

因此,在 bar::~Bar::foo::~Foo 完成之后,才会启动 bar::~Bar 的初始化,这是一个矛盾点。
唯一的解释可能是认为[basic.start.term]/1仅适用于在程序/线程终止时生存期已经开始的对象,但与[stmt.dcl]相反,其规定如下:
5 - 具有静态或线程存储期的块作用域对象的析构函数将仅在构造时执行。[注意:[basic.start.term]描述了以静态和线程存储期的块作用域对象被销毁的顺序。—注]
这显然只旨在适用于正常的线程和程序终止,即从main函数或线程函数返回或通过调用std :: exit。
此外,[basic.stc.thread]规定如下:
具有线程存储期的变量应在第一次odr-use([basic.def.odr])之前初始化,并且如果构造,则应在线程退出时销毁。
这里的“应”是对实现者的指示,而不是对用户的指示。
请注意,开始销毁范围为析构函数的thread_local的生命周期是没有问题的,因为[basic.start.term]/2不适用于它(它之前没有被销毁)。这就是我认为当你允许程序继续正常完成时会发生未定义行为的原因。

类似的问题之前已经被问过,不过是关于静态存储期和静态对比而非 thread_local 对比静态;具有静态存储期的对象的销毁(以及https://groups.google.com/forum/#!topic/comp.std.c++/Tunyu2IJ6w0)和 在另一个静态对象的析构函数中构造的静态对象的析构函数。我倾向于在后一个问题上同意James Kanze的看法,即[defns.undefined]适用于此处,并且行为未定义,因为标准没有定义它。最好的方法是由有地位的人开启一个缺陷报告(涵盖在staticthread_local的析构函数内初始化的所有组合),希望得到明确的答案。


0

请将您的程序编写为:

#include <iostream>

thread_local struct Foo {
    Foo() { std::cout << "Foo()\n"; }
    ~Foo() { std::cout << "~Foo()\n"; }
} t;
struct Bar {
    Bar() { std::cout << "Bar()\n"; }
    ~Bar() { std::cout << "~Bar()\n"; t; }
} b;

int main() {
    return 0;
}

如果Foo不是线程本地的,那么Foo tBar b处于相等的位置,可以在Bar b之前销毁Foo t
在这种情况下,在b.~Bar()中引用t时,它引用的是一个已经被销毁的结构体,在某些系统上销毁结构体会释放其内存,这应该是未定义行为。
因此,添加thread_local仍然是未定义行为。

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