Search code examples
c++language-lawyerc++20lifetime

End of object lifetime if its storage is partially reused


Starting from C++20 and as of the latest standard draft, it appears like the rules for the end of an object's lifetime are not super clear when the object's storage is partially "re-used". The existing answers on StackOverflow that I've found are not fully conclusive on this matter.

Given the assumption that double and uint64_t alignment and storage space would be exactly the same, I've got the following question from an code example below:

will new (&a->c1) uint64_t{4} below end the lifetime of object a?

struct A
{
   double d;   
   int i;
   double c1;
   double d1;
   double d2;
};


int main () {

A* a = new A{};
uint64_t* i = new (&a->c1) uint64_t{4}; //will this end lifetime of a? It would clearly end the lifetime of a.c1
*i = 124; //this must be UB because of strict-aliasing rules
return 0;
}

The standard clearly stipulates the rules at the end of an object's lifetime:

http://eel.is/c++draft/basic.life#1.3

  • if T is a non-class type, the object is destroyed, or

  • if T is a class type, the destructor call starts, or

  • the storage which the object occupies is released, or is reused by an object that is not nested within o ([intro.object]).

Then formally the object is nested in the following conditions:

An object a is nested within another object b if: (4.1) a is a subobject of b, or (4.2) b provides storage for a, or (4.3) there exists an object c where a is nested within c, and c is nested within b.

For our code example we are bound to

subobject of b, or (4.2) b provides storage for a, or (4.3) there

Then the standard defines what provides storage means

If a complete object is created ([expr.new]) in storage associated with another object e of type “array of N unsigned char” or of type “array of N std​::​byte” ([cstddef.syn]), that array provides storage for the created object if: (3.1) the lifetime of e has begun and not ended, and (3.2) the storage for the new object fits entirely within e, and (3.3) there is no array object that satisfies these constraints nested within e.

This implies that only arrays of std::byte or unsigned char can provide storage...From which I imply that creating uint64_t{4} object inside a's storage would end the lifetime of object a, as uint64_t{4} is not nested in a...Although I'm not really sure about this conclusion.


Solution

  • will new (&a->c1) uint64_t{4} below end the lifetime of object a?

    Not of the object associated with the variable a, but the object that a points to, i.e. the A object, not the A* object. Yes, the A object's lifetime will end with reuse of (part of) its storage in the new-expression because the object reusing its storage isn't nested within it. The c1 subobject of the A object will also have its lifetime ended since the new object reuses its storage as well without being nested in it. I would also expect that this would end the lifetime of all other subobjects of the A object, however currently the standard doesn't seem to say that.

    *i = 124; //this must be UB because of strict-aliasing rules
    

    That's well-defined. i is pointing to an object of type uint64_t that you created in the previous new-expression. So it can't be an aliasing violation.

    After the new-expression you can't use the object that a points to any more. In particular you can't access a.c1 any more, because that (sub)object's lifetime has also ended and you can't access the same memory location through reinterpret_cast<double*>(i) because that would indeed be an aliasing violation.