7

After answering this question and not finding a satisfying answer in the standard paper, I started wondering. The standard states the following w.r.t. initialization of mentioned variables:

§6.7 [stmt.dcl] p4

[...] Otherwise such a variable is initialized the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization. If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration.

There is no mentioning of what might cause the initialization to be retried if it failed by anything else than throwing an exception (longjmp(), thead exit, signals to name a few).

Did I overlook anything in the standard? I looked through initialization, declaration and exception clauses over and over and even consulted the CWG defects table of content with a quick search for "static", but couldn't find anything related.

Is this an underspecification (and as such a defect) in the standard?

Community
  • 1
  • 1
Xeo
  • 123,374
  • 44
  • 277
  • 381
  • 1
    Does C++ define _anything_ relating to signals? Or `longjmp`? Thread exit might change things. – Lightness Races in Orbit Jan 30 '12 at 00:49
  • Do you have any concrete scenario in mind that you don't find adequately specified by the standard? – Kerrek SB Jan 30 '12 at 00:51
  • @Kerrek: The linked question. :P – Xeo Jan 30 '12 at 00:52
  • @Lightness: 18.10/4 for `setjmp`/`longjmp`. See also this: http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#28 – Xeo Jan 30 '12 at 00:53
  • @Xeo: Hmm... I don't quite see what's ambiguous about that question. Could you perhaps elaborate why you don't think the behaviour is sufficiently well specified? – Kerrek SB Jan 30 '12 at 00:55
  • @Kerrek: It's just not specified if the initialization will be retried if it fails through anything else than an exception. – Xeo Jan 30 '12 at 01:04
  • @Xeo: How else can it fail? What you perceive as a problem is not pertinent to `static` variables. You can ask the same for *any* other object: How do you know if an object is initialized if no exception is thrown? – Kerrek SB Jan 30 '12 at 01:10
  • @Kerrek: Like I said in the question, `longjmp`, signals, `ExitThread` amongst others. Also, this is specific to `static`/`thread_local` local variables, since they'll only be fully initialized *once*. Other objects will always be initialized over and over (since they're always destroyed). – Xeo Jan 30 '12 at 01:14
  • @Xeo: But surely if you `longjmp` or `ExitThread` out of **any** object construction, you're in trouble, non? Isn't that always UB? – Kerrek SB Jan 30 '12 at 02:51
  • @Xeo: When you list other ways for a function to exit, what do you mean by "signals"? That's not a part of the C++ specification. – Nicol Bolas Jan 30 '12 at 03:30
  • @Kerrek: You'll be in trouble most of the time with `longjmp`, yes. See the paragraph I mentioned in reply to Lightness. However, like said, automatic objects will be initialized again anyways everytime their declaration is passed, which is not the case for static and thread-local (which are implicitly static btw) local objects. I just want to know when the construction will be retried, mostly to give an adequate answer to the linked question. :) – Xeo Jan 30 '12 at 10:53
  • @Nicol: C++ inherited signals from C, so it's indirectly part of its specification. – Xeo Jan 30 '12 at 10:58

2 Answers2

2

The C++ specification can only define things that are contained within the C++ specification. Remember: the C++ specification defines the behavior of a virtual machine it defines. And if it doesn't define that something can happen, it certainly doesn't define the behavior of C++ around that something that it doesn't say can happen.

According to the C++ specification, a thread can exit in exactly three ways: by returning from its main function, throwing an exception through its main function, and direct process exiting (as with std::terminate or similar functions). In short, a C++ thread cannot exit in any other way. There is no ExitThread function in standard C++. Similarly, std::thread cannot kill a thread, either externally or internally.

Therefore, anything that does cause this thing that C++ says can't happen is, by definition undefined. I guess it wouldn't even be "undefined behavior"; it would be in that nebulous space that threading was in before C++11 actually laid down how thread interactions worked.

The same goes for "signals", whatever those are. The C++ spec doesn't say that those can cause a function to exit. Here be dragons.

As for longjmp, that's covered by the behavior of longjmp. When you use longjmp to exit a function, that function never finished, just as if you used throw and catch. And in C++, an object is only constructed when its constructor has completed. Therefore the object's initialization was never completed, and it is uninitialized.

I don't have the ISO C specification (which C++ references for the behavior of longjmp), but C++11 does strongly suggest that you can equate throw/catch with longjmp/setjmp, as far as when you get undefined behavior:

§18.10 [support.runtime] p4:

The function signature longjmp(jmp_buf jbuf, int val) has more restricted behavior in this International Standard. A setjmp/longjmp call pair has undefined behavior if replacing the setjmp and longjmp by catch and throw would invoke any non-trivial destructors for any automatic objects.

So I don't think this is underspecified. It may not be nicely and neatly laid out, but all of the pieces are there.

Nicol Bolas
  • 378,677
  • 53
  • 635
  • 829
0

Just because the text mentions one particular case doesn't imply by omission that others would be any different. If there are other ways to prevent the initialization from completing, the implementation must retry at the next execution.

I think Nicol's answer is mostly correct, but a non-trivial constructor does not imply a non-trivial destructor. longjmp may therefore interrupt initialization such that it must be retried. This is tricky only in a multithreaded environment, where a mutex is needed to prevent a race condition between threads vying to be the first to execute the initialization. The phantom mutex object needs a non-trivial destructor even if the initialized object does not have one. The likely result is deadlock. This is probably good material for a DR.

Potatoswatter
  • 126,977
  • 21
  • 238
  • 404
  • It's not that the standard only lists one case, it only specifies retry *exactly for that case*. It doesn't say "If failed, retry next time around". It specifically says "If failed *by throwing*, retry next time around". – Xeo Jan 30 '12 at 16:23
  • @Xeo: Not at all. If the initialization did not complete, it wasn't an initialization in the first place. The retry *is* the first time. That the preceding text already implies this is revealed by the language, "the initialization is not complete, **so** it will be tried again…" – Potatoswatter Jan 30 '12 at 16:43
  • No it does not, because the text before *that* mentions only exceptions. You can't just detach both parts. In any case, I think it's a good candidate for atleast a clarification. Btw, on the mutex issue, I think this foot note on page 138 may apply: "88) The implementation must not introduce any deadlock around execution of the initializer." – Xeo Jan 30 '12 at 21:43
  • @Xeo: The part of that sentence I omitted refers to exceptions, of course. The text before *that* does not mention exceptions. The behavior that initialization is retried is implied completely by "Otherwise such a variable is initialized the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization." In other words, passing only partway through the initialization counts for nothing, and the second time still counts as the first time. Also, the very notion of a "try" is only introduced by "tried again." – Potatoswatter Jan 31 '12 at 00:25