What is called first: destructor of static object or atexit handler?

538 Views Asked by At

Is the order guaranteed in this case or is it UB?

#include <iostream>
#include <cassert>

using namespace std;

struct TraceHelper
{
    TraceHelper()
    {
        cout << "TraceHelper::constructor()" << endl;
    }

    ~TraceHelper()
    {
        cout << "TraceHelper::destructor()" << endl;
    }
};

void trace_fn()
{
    static TraceHelper th;
    cout << "trace_fn()" << endl;
}

void my_atexit()
{
    cout << "my_atexit()" << endl;
}

int main()
{
    cout << "Entered main()" << endl;
    assert(0 == atexit(my_atexit));
    trace_fn();
    trace_fn();
    cout << "Exiting main()" << endl;

    return 0;
}

The output:

Entered main()
TraceHelper::constructor()
trace_fn()
trace_fn()
Exiting main()
TraceHelper::destructor()
my_atexit()
2

There are 2 best solutions below

2
Hari On

What we are looking here is the interaction between the main function and std::exit:

Returning from the main function, [...] performs the normal function termination (calls the destructors of the variables with automatic storage durations) and then executes std::exit, passing the argument of the return statement (or ​0​ if implicit return was used) as exit_code.

As far as destruction of static objects are concerned, what is relevant to the question are the following lines:

If the completion of the initialization of a static object A was sequenced-before the call to std::atexit for some function F, the call to F during termination is sequenced-before the start of the destruction of A.

If the call to std::atexit for some function F was sequenced-before the completion of initialization of a static object A, the start of the destruction of A is sequenced-before the call to F during termination.

In the code in the question, the function my_atexit() is registered using std::atexit() before the creation of the static object th in the function trace_fn(). Thus, the call of the destructor of TraceHelper is happening before calling my_atexit(). The behavior is as expected.

5
Adrian Mole On

In your specific case (i.e., that of a local static object), cppreference has this to say (in the "since C++11" part):

  1. The destructors of objects with thread local storage duration that are associated with the current thread, the destructors of objects with static storage duration, and the functions registered with std::atexit are executed concurrently, while maintaining the following guarantees:

    c) If the completion of the initialization of a static object A was sequenced-before the call to std::atexit for some function F, the call to F during termination is sequenced-before the start of the destruction of A
    d) If the call to std::atexit for some function F was sequenced-before the completion of initialization of a static object A, the start of the destruction of A is sequenced-before the call to F during termination.

Thus, since th is initialized – by definition – the first time execution passes through it's declaration, then your call to atexit will be fully sequenced before the first call to trace_fn() and the consquent construction of tf. Thus, paragraph "d" in the above citation holds and the destruction/termination sequence you see is well-defined.

Here is the 'equivalent' section from this Draft C++17 Standard:

6.8.3.4 Termination      [basic.start.term]


5    If the completion of the initialization of an object with static storage duration strongly happens before a call to std::atexit, the call to the function passed to std::atexit is sequenced before the call to the destructor for the object. If a call to std::atexit strongly happens before the completion of the initialization of an object with static storage duration, the call to the destructor for the object is sequenced before the call to the function passed to std::atexit. …

(Here is the equivalent section from the latest, on-line Draft C++ Standard.)

Note, also, that the destructors of such block scope static objects and calls to registered atexit handlers are called 'concurrently' (see the comments on possible misuse of this term), with the provided guarantees, so you can have calls to multiple destructors 'intermingled' with calls to multiple exit handlers.