Zebrafish
Zebrafish

Reputation: 13886

Is accessing memory after a destructor call undefined behavior?

I'm wondering if the following is undefined?

int main()
{
    struct Doggy { int a; ~Doggy() {} };
    Doggy* p = new Doggy[100];

    p[50].~Doggy();

    p[50].a = 3; // Is this not allowed? The destructor was called on an
// object occupying that area of memory.
// Can I access it safely?
    if (p[50].a == 3);
}

I guess this is generally good to know, but the reason I'm specifically wanting to know is that I have a data structure consisting of an array, where the buckets can be nullable by setting a value, kind of like buckets in a hash table array. And when the bucket is emptied the destructor is called, but then checking and setting the null state after the destructor is called I'm wondering if it's illegal.

To elaborate a little, say I have an array of objects and each object can be made to represent null in each bucket, such as:

struct Handle
{
    int value = 0; // Zero is null value
    ~Handle(){}
};

int main()
{
    Handle* p = new Handle[100];
    // Remove object 50
    p[50].~Handle();
    p[50].value = 0; // Set to null
    if (p[50].value == 0) ; // Then it's null, can I count on this?
   // Is this defined? I'm accessing memory that was occupied by
   // object that was destroyed.
}

Upvotes: 3

Views: 253

Answers (2)

Ted Lyngmo
Ted Lyngmo

Reputation: 117288

Yes it'll be UB:

[class.dtor/19]

Once a destructor is invoked for an object, the object's lifetime ends; the behavior is undefined if the destructor is invoked for an object whose lifetime has ended ([basic.life]).

[Example 2: If the destructor for an object with automatic storage duration is explicitly invoked, and the block is subsequently left in a manner that would ordinarily invoke implicit destruction of the object, the behavior is undefined. — end example]

p[50].~Handle(); and later delete[] p; will make it call the destructor for an object whose lifetime has ended.

For p[50].value = 0; after the lifetime of the object has ended, this applies:

[basic.life/6]

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.allocation]), and using the pointer as if the pointer were of type void* is well-defined. Indirection through such a pointer is permitted but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:

6.2 - the pointer is used to access a non-static data member or call a non-static member function of the object

Upvotes: 5

Ext3h
Ext3h

Reputation: 6393

Yes, it's mostly. Handle::value is just an offset to a pointer of type Handle, so it's just going to work wherever you point it to, even if the containing object isn't currently constructed. If you were to use anything with virtual keyword, this would end up broken though.

p[50].~Handle(); this however is a different beast. You should never invoke destructors manually unless you have also explicitly invoked the constructor with placement new. Still not illegal, but dangerous.

delete[] p; (omitted in your example!) is where you end up with double-destruction, at which point you are well beyond UB, straight up in the "it's broken" domain.

Upvotes: 3

Related Questions