Open frederick-vs-ja opened 1 year ago
[basic.life#9] might be problematic, it seems. Not only is it a normative redundancy, the intention behind it (as I understand it at least) is (mostly) formally unreachable through the current wording.
If a program ends the lifetime of an object of type T with static ([basic.stc.static]), thread ([basic.stc.thread]), or automatic ([basic.stc.auto]) storage duration and if T has a non-trivial destructor, and another object of the original type does not occupy that same storage location when the implicit destructor call takes place, the behavior of the program is undefined. This is true even if the block is exited with an exception.
So, it might seem that the underlying intention was to allow implicit calls of trivial destructors on out-of-life (explicitly destroyed beforehand) objects under variables. However, in no ways the clause attempts to override (and, as it seemingly turns out, complement) anything previously or subsequently stated: it mostly constitutes an emphasis on a subset of cases. After reevaluating the wording for a bit, I would argue that the majority of such cases, including those not covered by the aforementioned clause, still cause undefined behavior (so clang might by partially right after all). The reasoning goes as follows (for the sake of simplicity, considering only objects with automatic storage duration):
{
struct S
{ };
S s /* #1 */;
s.~S() /* #2 */;
} /* #3 */
1. Objects are created by definitions, their lifetimes begin upon allocating the necessary storage and completing the initialization. Lifetimes of objects of class types end when the corresponding destructor calls start. Thus, in the example above, at #1
the lifetime of the object under s
begins and at #2
it ends.
2. A glval that used to refer to an object whose lifetime subsequently ended refers to a region of storage the object in question used to occupy (1, 2). Using such a glval to call a non-static member function results in undefined behavior. Destructors constitute non-static member functions.
3. A variable that belongs to a block scope has automatic storage duration (implying that the object under it does so too; I'd say the two are used excessively interchangeably, to the point of diminished clarity of the text). When a variable goes inactive due to the flow of control escaping the block it belonged to, it gets destroyed (1, 2), which for an object [under it] of a class type warrants a destructor call, given the object's initial construction finished uninterrupted. Thus, at #3
for the (now dead) object under s
a destructor gets called for the second time, presumably invoking undefined behavior due to the reasoning expressed under (2)
.
There might be a small loophole out of all this: I've found no explicit enough link that would imply this second destructor call happening through the use of the variable (and consequently, the glval it represents, although there is a statement about the variable itself being destroyed there). Personally, I'd deem this excuse unconvincing, since, as I understand it, the objects themselves were not meant to be used in an out-of-life state (consider this clause and the accompanying note, both of which, I'd say, support this view). Constexpr evaluation of the example above from the leading implementations (godbolt.org): clang diagnoses it, the other two do not.
Now, there indeed exists another way to end the lifetime of an object: by reusing its storage, as is the case in the accompanying example. Again, assuming the implicit destructor call happens through the glval constituting the name of the defined variable, the reasoning boils down to the notion of transparent replaceability: the defining requirement stating that both the replaced and the replacing objects must be of the same type (disregarding the top-level cv-qualification), which is basically what [basic.life#9] redundantly wants. Otherwise, the original glval won't bind to the newly created object and the behavior of calling a non-static member function through it would be equally undefined, for the very same reasons already described under (2)
(the glval would still refer to the region of storage, not to the object).
The above, however, concerns only the objects of class types. For the fundamental object types (2)
doesn't hold (no non-static member function is called, no object is otherwise accessed), so I've came across no indication of the notion that destroying such objects twice should constitute an undefined operation (although I would most likely prefer a more coherent statement in that regard). It would be fair to note that clang doesn't discriminate between such cases and diagnoses them regardless of the type of the object involved: depending on one's view onto the more obscure clauses (like [basic.life#4]), this might be incorrect.
To summarize, [basic.life#9] most likely is indeed redundant (along with [class.dtor#18] - nothing changes in that regard). If there was an intention to make trivial "double-destructions" well-defined, it doesn't seem to be what is formally allowed at the moment, however destroying a non-class-type object twice doesn't appear to be associated with undefined behavior. In any case, would greatly appreciate more perspectives on the issue raised, of course.
[basic.life]/9 and [class.dtor]/18 seem to be normatively redundant, because
Originally discovered by @sergey-anisimov-dev in https://github.com/cplusplus/CWG/issues/361#issuecomment-1634583516.