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.
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.
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).
that is not safe.
The initialization of
xstrongly happens before the use ofxhere, but where it happens is implementation defined.The compiler is free to have
mainwait while it spawns a new thread, that new thread instantiatesx, 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
xbefore thetry, 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
mainhas 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 tox, but access to any symbol inx'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.