-
Notifications
You must be signed in to change notification settings - Fork 772
[basic.life] Remove description of impossible UB CWG2523 #2342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
A destructor that is not called cannot produce side effects, therefore it is impossible to depend on those side effects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nack.
It's talking about the side effects that would have been produced by the destructor. If the program's correctness relies on side effects of the destructor, and you don't run the destructor, the program is not correct.
That "shall" should probably be replaced, so "is not implicitly called". |
How is it possible for a program to rely on something guaranteed not to happen? If the destructor's side effects would have prevented some other UB, then obviously there is UB, but it seems odd to state that here since the UB must have been triggered by violating some other requirement. Unless you are implying that this clause is trying to say that behavior is undefined if a program produces an incorrect result due to not calling a destructor without otherwise committing any UB. |
Are you suggesting that without this phrase in the Standard text, readers might assume that the side effects of a destructor occur even when that destructor is never called? At best, this phrase deserves to be a note. |
What does the phrase "depend on" mean here? "The program's correctness" also sounds strange. What does "correctness" here mean exactly, besides "free from UB"? If there is UB, the program is definitely not correct to be a portable program. But how can the specification of a language guarantee the correctness of a program without any concrete expectation from the user? |
I don't see what's so hard to understand. If you have a destructor that performs an important operation, say unlocking a mutex, and if you fail to invoke that destructor for one of your objects, then your program might not work correctly. It might deadlock because other users of that mutex can never lock it. The point is that if your destructor doesn't do anything important, it's OK if the object is never destroyed. On the other hand, if you rely on resources being freed (to prevent memory exhaustion, or mutex deadlocks, or whatever) then you'd better destroy your objects not leak them. That needs to be said somehow, even if only as a note. |
No. But readers could assume it's undefined to fail to destroy any object with a non-trivial destructor, even if that destructor has no side effects. That's not a completely unreasonable position. The current wording is poor and should be improved, but not just removed. |
That is exactly why I think this line needs to be deleted, or at least reworded to not imply UB in a case where the behavior is actually quite well defined. If a reader makes that assumption, then they are misinterpreting the standard (I think, does anyone in CWG claim that this clause adds normative UB?)
Conversely, it is possible to rely on the resources not being freed in some cases. If I actively choose not to call the dtor, I can rely on the effects not occurring. Or at least that is how I interpret the prior clause of the sentence. In those cases, I don't want the specter of UB dangling there in a perfectly well defined case. What would you want the note to say? I am struggling to think of anything that needs to be added to "the destructor shall not be implicitly called." That seems to clearly state the defined behavior, with little room for misinterpretation. |
I think the first half of this sentence:
doesn't allow that interpretation, although it would be clearer if we were to strike the word "explicitly." AFAICS the second half of the sentence exists solely to inform us that implementations don't magically invoke destructors for objects whose lifetime is ended by reusing their storage. What do we all think of: For an object of a class type with a non-trivial destructor, the program is not required to
call the destructor
-explicitly
before the storage which the object occupies is reused or released
+. [Note: The destructor of such an object is not called implicitly.-end note]
-; however, if there is no explicit call to the destructor or if a delete-expression
-(7.6.2.5) is not used to release the storage, the destructor shall not be implicitly called
-and any program that depends on the side effects produced by the destructor has
-undefined behavior. |
Another, harsher, interpretation of the "undefined behavior" part is that your compiler can rely on the user explicitly calling the destructor for each constructed object before the user ends the lifetime by memory reuse. The suggestions to strike this part from the wording would be a normative change, unfit for an editorial issue. Feel free to update the pull request with a change for "shall" -> "is" and inserting "would" for the destructor call that doesn't actually happen. Anything beyond that seems to be CWG material; please talk to the -core reflector if you still feel there is an issue. |
Leaving editorial scope, I would actually be in favor of turning all "ends lifetime by memory reuse" situations for objects with non-trivial destructors into undefined behavior. This seems the clearer rule and much more in line with our enhanced memory model (e.g. pointers are more than addresses etc.) |
I'm ambivalent. I can imagine using a |
@jwakely: Right, but if you just release() the entire storage range, aren't you relying on the fact that the destructors of the contained objects are trivial? Otherwise, you might be leaking memory or file descriptors or mutex locks. On the other hand, if your destructors are, in fact, trivial, nothing is lost by calling them (they will be no-ops anyway, and the calls very likely optimized away). |
They could be empty, but not trivial. Or they might have side effects, but sometimes you might explicitly want to avoid the side effects. Maybe the destructors perform a "commit" step, and if you want to abandon the set of operations without committing, you just "leak" all the objects, so nothing is committed. I'm not saying that's the best way to design the system, but I bet somebody's done it, and do we gain anything by making that undefined? As long as nobody ever tries to access the old objects after the storage is reused, it will Just Work. (This of course brings up the question again of what it means to "depend" on the side effects, or to depend on the lack of them.) |
I can guarantee at least one large scale C++ user has a non-trivial amount of code that relies on ending the lifetime of objects (with non-trivial destructors) by re-using storage is well-defined behavior. This is explicitly one of the motivations behind re-using memory pools with pmr allocators, where you determine everything in the pool has destructors that only release memory also in the pool - and so simply reclaim the pool when the whole data structure is no longer needed. Any change introducing broad UB here would be Core/EWG material, and likely contentious. |
Editorial meeting: The standard rules cannot depend on the programmer's intent. Pass to CWG with the intent of applying as-is. |
Edit: CWG2523 is modified now to fully address this issue. |
CWG2523 has been applied. |
A destructor that is not called cannot produce side effects, therefore it is impossible to depend on those side effects.