3

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.
}
Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
Zebrafish
  • 11,682
  • 3
  • 43
  • 119
  • Are you using placement new to create the buckets? – Retired Ninja Aug 11 '21 at 05:36
  • Yes I am if a new object is created in place. But I'm wondering because the object itself may be nullable and whether an object exists in the bucket (the value) in the map can be checked with a member in the object itself that's set to a null value. But I'm wondering if this can be checked for null after the destructor has been called, like in my above example. – Zebrafish Aug 11 '21 at 05:39
  • Seems like `std::optional` might be a useful thing to use here. – Jeremy Friesner Aug 11 '21 at 05:48
  • Yes, I'm probably going to use something like that, but I'm interested in knowing the answer to this from a technical standpoint. Something like optional will keep a bool, which for an 8-byte struct will waste probably 7 bytes per object. This is no big deal generally, but if a null value can be stored in the struct itself I'm interested in knowing the answer to this question. – Zebrafish Aug 11 '21 at 05:51
  • https://stackoverflow.com/questions/6500313/why-should-c-programmers-minimize-use-of-new -- just btw. In particular using vector new is a code smell. – Ulrich Eckhardt Aug 11 '21 at 06:09

2 Answers2

5

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

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • 2
    @Elliott You must not mix `new[]` with `free`, that's two entirely different APIs. – Ext3h Aug 11 '21 at 06:28
  • 1
    @Ext3h: Thanks, [I was wrong](https://isocpp.org/wiki/faq/freestore-mgmt#mixing-malloc-and-delete): `conceptually malloc and new allocate from different heaps, so can’t free or delete each other’s memory.` I'll delete this comment later on. – Elliott Aug 11 '21 at 06:58
  • 1
    This is exactly the information I was looking for, it clearly is UB. – Zebrafish Aug 11 '21 at 16:48
  • @Zebrafish Indeed! Glad to help! – Ted Lyngmo Aug 11 '21 at 16:49
3

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.

Ext3h
  • 5,713
  • 17
  • 43
  • Just playing devil's advocate here, but just because a program is guaranteed to cause a seg. fault doesn't mean that it's undefined. The same if there's a memory leak. – Elliott Aug 11 '21 at 06:30
  • @Elliott Undefined behavior is a formal term. You're right, there are well-defined ways to raise SIGSEGV into a process, but this is not that. – GManNickG Aug 11 '21 at 06:47