Do trivial destructors cause aliasing?

advertisements

C++11 §3.8.1 declares that, for an object with a trivial destructor, I can end its lifespan by assigning to its storage. I am wondering if trivial destructors can prolong the object's lifespan and cause aliasing woes by "destroying an object" that I ended the lifespan of much earlier.

To start, something which I know is safe and alias-free

void* mem = malloc(sizeof(int));
int*  asInt = (int*)mem;
*asInt = 1; // the object '1' is now alive, trivial constructor + assignment
short*  asShort = (short*)mem;
*asShort = 2; // the object '1' ends its life, because I reassigned to its storage
              // the object '2' is now alive, trivial constructor + assignment
free(mem);    // the object '2' ends its life because its storage was released

Now, for something which is not so clear:

{
    int asInt = 3; // the object '3' is now alive, trivial constructor + assignment
    short* asShort = (short*)&asInt; // just creating a pointer
    *asShort = 4; // the object '3' ends its life, because I reassigned to its storage
                  // the object '4' is now alive, trivial constructor + assignment
    // implicitly, asInt->~int() gets called here, as a trivial destructor
}   // 'the object '4' ends its life, because its storage was released

§6.7.2 states that objects of automatic storage duration are destroyed at the end of the scope, indicating that the destructor gets called. If there is an int to destroy, *asShort = 2 is an aliasing violation because I am dereferencing a pointer of unrelated type. But if the integer's lifespan ended before *asShort = 2, then I am calling an int destructor on a short.

I see several competing sections regarding this:

§3.8.8 reads

If a program ends the lifetime of an object of type T with static (3.7.1), thread (3.7.2), or automatic (3.7.3) storage duration and if T has a non-trivial destructor,39 the program must ensure that an object of the original type occupies that same storage location when the implicit destructor call takes place; otherwise the behavior of the program is undefined.

The fact that they call out types T with non-trivial destructor as yielding undefined behavior seems, to me, to indicate that having a different type in that storage location with a trivial destructor is defined, but I couldn't find anywhere in the spec that defined that.

Such a definition would be easy if a trivial destructor was defined to be a noop, but there's remarkably little in the spec about them.

§6.7.3 indicates that goto's are allowed to jump into and out of scopes whose variables have trivial constructors and trivial destructors. This seems to suggest a pattern where trivial destructors are allowed to be skipped, but the earlier section from the spec on destroying objects at the end of the scope mentions none of this.

Finally, there's the sassy reading:

§3.8.1 indicates that I am allowed to start an object's lifespan any time I want, if its constructor is trivial. This seems to indicate that I could do something like

{
    int asInt = 3;
    short* asShort = (short*)&asInt;
    *asShort = 4; // the object '4' is now alive, trivial constructor + assignment
    // I declare that an object in the storage of &asInt of type int is
    // created with an undefined value.  Doing so reuses the space of
    // the object '4', ending its life.

    // implicitly, asInt->~int() gets called here, as a trivial destructor
}

The only one of these reading that seems to suggest any aliasing issues is §6.7.2 on its own. It seems like, when read as part of a whole spec, the trivial destructor should not affect the program in any way (though for various reasons). Does anyone know what happens in this situation?


In your second code snippet:

{
    int asInt = 3; // the object '3' is now alive, trivial constructor + assignment
    short* asShort = (short*)&asInt; // just creating a pointer
    *asShort = 4;
    // Violation of strict aliasing. Undefined behavior. End of.
}

The same applies to your first code snippet. It is not "safe", but it will generally work because (a) there's no particular reason for a compiler to be implemented such that it doesn't work, and (b) in practice compilers have to support at least a few violations of strict aliasing or else it would be impossible to implement the memory allocator using the compiler.

The thing that I know can and does provoke compilers to break this kind of code is if you read asInt afterwards, the DFA is allowed to "detect" that asInt is not modified (since it's modified only by the strict-alias violation, which is UB), and move the initialization of asInt after the write to *asShort. That's UB by either of our interpretations of the standard though -- in my interpretation because of the strict aliasing violation and in your interpretation because asInt is read after the end of its lifetime. So we're both happy for that not to work.

However I don't agree with your interpretation. If you consider that assigning to part of the storage of asInt ends the lifetime of asInt, then that's a direct contradiction of the statement that the lifetime of an automatic object is its scope. OK, so we might accept that this is an exception to the general rule. But that would mean that the following is not valid:

{
    int asInt = 0;
    unsigned char *asChar = (unsigned char*)&asInt;
    *asChar = 0; // I've assigned the storage, so I've ended the lifetime, right?
    std::cout << asInt; // using an object after end of lifetime, undefined behavior!
}

Except that the whole point of allowing unsigned char as an aliasing type (and of defining that all-bits-0 means "0" for integer types) is to make code like this work. So I'm very reluctant to make an interpretation of any part of the standard, which implies that this doesn't work.

Ben gives another interpretation in comments below, that the *asShort assignment simply doesn't end the lifetime of asInt.