这个程序:
#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()。这是预期的行为吗?
答案 0 :(得分:9)
本标准不包括此案;最严格的解释是,在具有静态存储持续时间的对象的析构函数中初始化thread_local
是合法的,但允许程序继续正常完成是非法的。
问题出现在[basic.start.term]:
1 - 具有静态存储持续时间的初始化对象(即,其生命周期([basic.life])已开始的对象的析构函数([class.dtor])被调用为从main返回的结果,因此调用std :: exit([support.start.term])。具有给定线程内的线程存储持续时间的初始化对象的析构函数被调用作为从该线程的初始函数返回的结果,并且作为该线程调用std :: exit的结果。在具有静态存储持续时间的任何对象的析构函数的启动之前,对具有该线程内的线程存储持续时间的所有初始化对象的析构函数的完成进行排序。 [...]
所以bar::~Bar::foo::~Foo
的完成在bar::~Bar
开始之前被排序,这是一个矛盾。
唯一的结果可能是认为[basic.start.term] / 1仅适用于生命周期在程序/线程终止时开始的对象,但是 contra {{ 3}}有:
5 - 当且仅当构造时,将执行具有静态或线程存储持续时间的块范围对象的析构函数。 [注意:[basic.start.term]描述了破坏具有静态和线程存储持续时间的块范围对象的顺序。 - 结束说明]
这显然只适用于普通线程和程序终止,通过从main或线程函数返回,或通过调用std::exit
。
此外,[stmt.dcl]还有:
具有线程存储持续时间的变量应在其第一次使用odr([basic.def.odr]之前)初始化,如果构造,则应在线程退出时销毁。
这里的“shall”是对实现者的指示,而不是对用户的指示。
请注意,开始析构函数范围thread_local
的生命周期没有任何问题,因为[basic.start.term] / 2不适用(以前没有销毁)。这就是为什么我认为当你允许程序继续正常完成时会发生未定义的行为。
之前已经提出了类似的问题,但关于静态与静态存储持续时间而不是thread_local
与静态存储的关系[basic.stc.thread](和Destruction of objects with static storage duration)和https://groups.google.com/forum/#!topic/comp.std.c++/Tunyu2IJ6w0。我倾向于同意James Kanze关于Destructor of a static object constructed within the destructor of another static object适用的后一个问题,并且行为未定义,因为标准没有定义它。最好的方法是让有资格的人打开缺陷报告(涵盖在static
和{{1}的析构函数中初始化的thread_local
和static
的所有组合。 s),希望得到明确的答案。
答案 1 :(得分: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
不是thread_local,则Foo t
和Bar b
处于相同位置,可以在Foo t
之前破坏Bar b
。
在这种情况下,当引用t
中的b.~Bar()
时,它是指一个被破坏的结构,该IMO应该是UB(在某些系统上,被破坏的结构释放了它的内存)。
因此,添加thread_local
仍然是未定义的行为