Jan Schultke
Jan Schultke

Reputation: 39678

Why are exceptions thrown during deferred dynamic initialization not caught by main?

The wording which confuses me is this:

It is implementation-defined whether the dynamic initialization of a non-block non-inline variable with static storage duration is sequenced before the first statement of main or is deferred. If it is deferred, it strongly happens before any non-initialization odr-use of any non-inline function or non-inline variable defined in the same translation unit as the variable to be initialized. It is implementation-defined in which threads and at which points in the program such deferred dynamic initialization occurs.

- [basic.start.dynamic] p5

In practice, this means that:

int do_throw() { throw 0; }

int x = do_throw(); // It is implementation-defined whether an exception is 
                    // thrown before main() or inside main().

int main() try {
    // an exception could be thrown here because we aren't using main()
    // (main can't be used, see [basic.start.main])
    return x;
} catch (...) {}

Note: merely defining main isn't odr-use of the function, see [basic.def.odr] p8. This makes it valid to sequence do_throw() inside main.

If the initialization is deferred, and the odr-use of x inside main triggers dynamic initialization inside main, it would be reasonable to believe that the function-try-block around main catches the exception. However:

Exceptions thrown in [...] or in constructors of objects associated with non-block variables with static storage duration are not caught by a function-try-block on the main function.

- [except.handle] p11

Contrary to intuition, the function-try-block on main would not catch the exception, even if the dynamic initialization of x is deferred.

Questions

Is this an oversight? Is it intentional? Do try blocks ignore deferred dynamic initializaton in general, or is it just the function-try-block on main?

How would you even implement this?! The function-try-block on main would have to selectively ignore some exceptions that originate from deferred dynamic initialization, but catch exceptions from other sources.

Further thoughts

The implied behavior is extremely surprising:

int main() try {
    throw 0; // safe
} catch (...) {}

int main() try {
    return x; // std::terminate is called because of an uncaught exception ?!?!
} catch (...) {}

int main() {
    try {
        return x; // safe ?!
    } catch(...) {}
}

Note: the try block is first statement inside main, so if deferring takes place, return x cannot be sequenced before try. I'm not sure if this is even implementable, because main only has one statement (unless we consider all statements recursively).

Upvotes: 2

Views: 109

Answers (2)

user17732522
user17732522

Reputation: 76708

It is irrelevant, because an exception thrown from the initialization of a non-block static storage duration variable always calls std::terminate according to [basic.start.dynamic]/8. You can't catch it anywhere anyway.

The only concern would be that std::terminate might be called at any unspecified point in main prior to the first odr-use and in any thread.

Upvotes: 0

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275650

try {
    return x; // safe ?!
} catch(...) {}

that is not safe.

If it is deferred, it strongly happens before any non-initialization odr-use of any non-inline function or non-inline variable defined in the same translation unit as the variable to be initialized. It is implementation-defined in which threads and at which points in the program such deferred dynamic initialization occurs.

The initialization of x strongly happens before the use of x here, but where it happens is implementation defined.

The compiler is free to have main wait while it spawns a new thread, that new thread instantiates x, an uncaught exception occurs, and the program terminates (I suspect it is legal for this extra thread to have no active handler? - but I could be wrong, in which case things get strange).

It is free to initialize x before the try, and similarly terminate the program.

...

However, I suspect this is not intended. I suspect that the function-try block clause exists because it there are implementations of C++ in which how static initialization occurs is that a table of such initialization is built (at compile time), and main has initialization code injected into it.

In such a model, having function-try blocks catch exceptions in that initialization makes perfect implementation sense. However, in other models it doesn't. So to deal with "do exceptions get caught when a static global fails to be constructed", they added that clause. You'll note that absent that clause and deferred initialization everything would be clear.

But deferred initialization and that clause together do act strange.

I suspect strongly that the only sane way to deal with an exception in a static global variable is std::terminate. Barring this to be "safe" you'd have to deal with the possibility of the first access to a symbol in a random file could throw an error (not just access to x, but access to any symbol in x's file!), which would require intense knowledge of the file-structure of every library to determine what exceptions you need to catch.

I think the deferred clause exists to allow for something like dynamic loading of translation units. So before you ODR-use anything in that file, all of the static globals are constructed; this might happen as the compiler loads the library containing the translation unit in question.

Annoyingly, it also permits elimination of static globals via translation unit garbage collection. If nobody ever accesses a symbol in a file, there is no ODR-use that requires happens-before initialization of the globals in the file.

Upvotes: 0

Related Questions