15

This program:

#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;
}

Prints

Bar()
~Bar()
Foo()

for me (GCC 6.1, Linux, x86-64). ~Foo() is never called. Is that the expected behaviour?

Tavian Barnes
  • 12,477
  • 4
  • 45
  • 118
  • legal or not, why would you do it ? – David Haim Jun 30 '16 at 17:59
  • 3
    @DavidHaim I'm attempting to implement part of `libc++abi` (`__cxa_thread_atexit()` in particular), and was curious if I'm supposed to handle this case or not. – Tavian Barnes Jun 30 '16 at 18:02
  • 5
    That's probably just `cout` getting destroyed before `foo`. Try throwing an exception from `Foo`'s destructor and see if `std::terminate` is called. – Baum mit Augen Jun 30 '16 at 18:03
  • [Works here](http://melpon.org/wandbox/permlink/lU6QsPK44jV6E4vH) – Kerrek SB Jun 30 '16 at 18:04
  • @BaummitAugen I replaced `cout` with `write(STDOUT_FILENO, ...)` and nothing changed. – Tavian Barnes Jun 30 '16 at 18:06
  • @BaummitAugen Isn't `cout` guaranteed to destroyed last since it is constructed first? – NathanOliver Jun 30 '16 at 18:06
  • @NathanOliver Different translation unit, so no. – Baum mit Augen Jun 30 '16 at 18:07
  • @NathanOliver `#include ` ensures that `std::c{in,out,err}` are not destroyed until after any globals in that file. But they may not be guaranteed to outlive `thread_local`s created while destroying the globals. – Tavian Barnes Jun 30 '16 at 18:08
  • @TavianBarnes What is `write`? – Baum mit Augen Jun 30 '16 at 18:09
  • 1
    @BaummitAugen See http://en.cppreference.com/w/cpp/io/ios_base/Init for how C++ works around the initialization order fiasco for `std::c{in,out,err}` – Tavian Barnes Jun 30 '16 at 18:09
  • @BaummitAugen http://linux.die.net/man/2/write – Tavian Barnes Jun 30 '16 at 18:09
  • @KerrekSB What version of `glibc` is in use on that machine? The behaviour may be different depending on if `__cxa_thread_atexit_impl()` is available in the C library. – Tavian Barnes Jun 30 '16 at 18:10
  • @NathanOliver Well, I'm not quite sure actually. – Baum mit Augen Jun 30 '16 at 18:13
  • @BaummitAugen Since it is all in one translation unit `cout` should be available right after `#include `. Anything after that should be destroyed before `cout`. – NathanOliver Jun 30 '16 at 18:14
  • @KerrekSB, it looks like your melpon link stopped yielding that initial result. If you re-run it now, it gets the same result as clang: `~Foo` is not called (perhaps as Tavian stated, it's a function of the libc/C++ std lib and not primarily the compiler. – Brian Cain Mar 06 '17 at 19:24
  • 1
    @BrianCain: Yes, that's consistent with the current language evolution, which aims to consider all thread-locals to be destroyed *before* statics, so that thread-local destructors can rely on the presence of static objects. – Kerrek SB Mar 06 '17 at 23:10

2 Answers2

9

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.

Similar questions have been asked before, though about static vs. static storage duration rather than thread_local vs. static; Destruction of objects with static storage duration (and https://groups.google.com/forum/#!topic/comp.std.c++/Tunyu2IJ6w0), and Destructor of a static object constructed within the destructor of another static object. I'm inclined to agree with James Kanze on the latter question that [defns.undefined] applies here, and the behavior is undefined because the Standard does not define it. The best way forward would be for someone with standing to open a defect report (covering all the combinations of statics and thread_locals initialized within the destructors of statics and thread_locals), to hope for a definitive answer.

Community
  • 1
  • 1
ecatmur
  • 152,476
  • 27
  • 293
  • 366
0

Write your program as

#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;
}

If Foo wasn't thread_local, then Foo t and Bar b are on equal position, and it's possible to destruct Foo t before Bar b.

In this case, when referring to t in b.~Bar(), it's referring to a destructed struct, which IMO should be a UB(on some system destructing struct free its memory).

Therefore, adding thread_local it's still undefined behavior

l4m2
  • 1,157
  • 5
  • 17