The Standard does not cover this case; the strictest reading would be that it is legal to initialize a thread_local in the destructor of an object with static storage duration, but it is illegal to allow the program to continue to normal completion.

The problem arises in [basic.start.term]:

1 - Destructors ([class.dtor]) for initialized objects (that is, objects whose lifetime ([basic.life]) has begun) with static storage duration are called as a result of returning from main and as a result of calling std::exit ([support.start.term]). Destructors for initialized objects with thread storage duration within a given thread are called as a result of returning from the initial function of that thread and as a result of that thread calling std::exit. The completions of the destructors for all initialized objects with thread storage duration within that thread are sequenced before the initiation of the destructors of any object with static storage duration. [...]

So the completion of bar::~Bar::foo::~Foo is sequenced before the initiation of bar::~Bar , which is a contradiction.

The only get-out could be to argue that [basic.start.term]/1 only applies to objects whose lifetime has begun at the point of program/thread termination, but contra [stmt.dcl] has:

5 - The destructor for a block-scope object with static or thread storage duration will be executed if and only if it was constructed. [ Note: [basic.start.term] describes the order in which block-scope objects with static and thread storage duration are destroyed. — end note ]

This is clearly intended to apply only to normal thread and program termination, by return from main or from a thread function, or by calling std::exit .

Also, [basic.stc.thread] has:

A variable with thread storage duration shall be initialized before its first odr-use ([basic.def.odr]) and, if constructed, shall be destroyed on thread exit.

The "shall" here is an instruction to the implementor, not to the user.

Note that there is nothing wrong with beginning the lifetime of the destructor-scoped thread_local , since [basic.start.term]/2 does not apply (it is not previously destroyed). That is why I believe that undefined behavior occurs when you allow the program to continue to normal completion.