Thread overview
Why dtor are not executed when removing a struct from associative arrays?
Sep 20, 2021
Learner
Sep 20, 2021
Tejas
Sep 20, 2021
Tejas
Sep 20, 2021
Tejas
Sep 21, 2021
Tejas
Sep 20, 2021
jfondren
Sep 20, 2021
Ali Çehreli
September 20, 2021

I was expecting something like going out of scope for that

import std.stdio;

struct S
{
    ~this()
    {
        writeln("S is being destructed");
    }
}

void main()
{
	S[int] aa;
    aa[1] = S();
    aa.remove(1);
    writeln("Why no dtor call on remove?");
}

I was expecting S instance dtor called
S is being destructed
September 20, 2021

On Monday, 20 September 2021 at 12:23:00 UTC, Learner wrote:

>

I was expecting something like going out of scope for that

import std.stdio;

struct S
{
    ~this()
    {
        writeln("S is being destructed");
    }
}

void main()
{
	S[int] aa;
    aa[1] = S();
    aa.remove(1);
    writeln("Why no dtor call on remove?");
}

I was expecting S instance dtor called
S is being destructed

I think it is being called:

import std.stdio;

struct S
{
	int a;
	this(int param){
		a = param;
	}
	~this()
	{
	    writeln("S(",a,") is being destructed");
	}
}

void main()
{
	S[int] aa;
        aa[1] = S(5);
        aa.remove(1);
        aa[1] = S(10);
        //writeln("Why no dtor call on remove?");
}

Output:
S(5) is being destructed
S(10) is being destructed

If the destructors were being executed based on the ending of scope, S(10) would have been destroyed first.

September 20, 2021

On Monday, 20 September 2021 at 12:23:00 UTC, Learner wrote:

>

I was expecting something like going out of scope for that

import std.stdio;

struct S
{
    ~this()
    {
        writeln("S is being destructed");
    }
}

void main()
{
	S[int] aa;
    aa[1] = S();
    aa.remove(1);
    writeln("Why no dtor call on remove?");
}

I was expecting S instance dtor called
S is being destructed

This looks to me like a bug, as

import core.memory : GC;
GC.collect;

immediately after the .remove will call the struct's destructor.

I only see https://issues.dlang.org/show_bug.cgi?id=20379 as related, though.

Here's another workaround:

alias purge = (kv, k) { kv[k] = typeof(kv[k]).init; kv.remove(k); };

with the same caveat of the .init structs also getting destructed later, you can use that in place of .remove.

September 20, 2021

On 9/20/21 8:23 AM, Learner wrote:

>

I was expecting something like going out of scope for that

import std.stdio;

struct S
{
     ~this()
     {
         writeln("S is being destructed");
     }
}

void main()
{
     S[int] aa;
     aa[1] = S();
     aa.remove(1);
     writeln("Why no dtor call on remove?");
}

I was expecting S instance dtor called
S is being destructed

AA values are not destroyed on removal. For a simple reason -- someone might still be referencing it.

struct S
{
   int x;
}
void main()
{
   S[int] aa;
   aa[1] = S(5);
   auto sptr = 1 in aa;
   sptr.x = 6;
   assert(aa[1].x == 6);
   aa.remove(1);
   assert(sptr.x == 6;
}

-Steve

September 20, 2021

On Monday, 20 September 2021 at 13:48:01 UTC, Tejas wrote:

>

On Monday, 20 September 2021 at 12:23:00 UTC, Learner wrote:

>

[...]

I think it is being called:

import std.stdio;

struct S
{
	int a;
	this(int param){
		a = param;
	}
	~this()
	{
	    writeln("S(",a,") is being destructed");
	}
}

void main()
{
	S[int] aa;
        aa[1] = S(5);
        aa.remove(1);
        aa[1] = S(10);
        //writeln("Why no dtor call on remove?");
}

Output:
S(5) is being destructed
S(10) is being destructed

If the destructors were being executed based on the ending of scope, S(10) would have been destroyed first.

Oh dear, I made a mistake :(
Sorry, shouldn't have commented out the writeln

September 20, 2021

On Monday, 20 September 2021 at 14:03:09 UTC, Tejas wrote:

>

On Monday, 20 September 2021 at 13:48:01 UTC, Tejas wrote:

>

On Monday, 20 September 2021 at 12:23:00 UTC, Learner wrote:

>

[...]

I think it is being called:

import std.stdio;

struct S
{
	int a;
	this(int param){
		a = param;
	}
	~this()
	{
	    writeln("S(",a,") is being destructed");
	}
}

void main()
{
	S[int] aa;
        aa[1] = S(5);
        aa.remove(1);
        aa[1] = S(10);
        //writeln("Why no dtor call on remove?");
}

Output:
S(5) is being destructed
S(10) is being destructed

If the destructors were being executed based on the ending of scope, S(10) would have been destroyed first.

Oh dear, I made a mistake :(
Sorry, shouldn't have commented out the writeln

To make up for my mistake:

In case you still want to delete stuff deterministically despite what Steve said, I suggest you make your struct a reference and use core.memory.__delete(not recommended to use this carelessly, btw)

import std.stdio;
import core.memory:__delete;

struct S
{
	int a;
	this(int param){
		a = param;
	}
	~this()
	{
	    writeln("S(",a,") is being destructed");
	}
}

void absoluteRemove(AA, keyType)(AA assArray, keyType key){
	__delete(assArray[key]);
}


void main()
{
	S*[int] aa;
        aa[1]  = new S(5);
        absoluteRemove(aa, 1);
        aa[2] = new S(10);
        absoluteRemove(aa, 2);
        writeln("Why no dtor call on remove?");
}
Output:
S(5) is being destructed
S(10) is being destructed
Why no dtor call on remove?

Because __delete cannot work on stack allocated objects

September 20, 2021
On 9/20/21 5:23 AM, Learner wrote:

> I was expecting S instance dtor called
> S is being destructed

If you are sure the element can be destroyed, you can call destroy():

import std.stdio;

enum someSpecialInitValue = 777;

struct S
{
  int i = someSpecialInitValue;

  this(int i)
  {
    this.i = i;
  }

  ~this()
  {
    writeln("S with ", i, " is being destructed");
  }
}

void destroyAndRemove(AA, Key)(AA aa, Key key) {
  auto found = key in aa;
  if (found) {
    destroy(*found);
    aa.remove(key);
  }
}

void main()
{
  S[int] aa;
  aa[1] = S(1);
  aa.destroyAndRemove(1);
  writeln("Actually, 2 dtor calls! :)");
}

destroy() puts the object to its initial state, which means, it gets destroyed again. That's why there are two destructor calls below for the same object. I used a special value to demonstrate the second destructor is called on the init state:

S with 1 is being destructed
Actually, 2 dtor calls! :)
S with 777 is being destructed

Ali
September 20, 2021

On 9/20/21 10:22 AM, Tejas wrote:

>

In case you still want to delete stuff deterministically despite what Steve said, I suggest you make your struct a reference and use core.memory.__delete(not recommended to use this carelessly, btw)

Do not call __delete here, use destroy. __delete will attempt to deallocate the block, which likely will fail since the key comes before the value, and GC.free on an interior pointer (I think) fails.

But if it succeeded, it would not be good. This leaves a dangling pointer inside the AA. Rehashing the AA likely would result in a memory corruption.

If you use destroy, the destructor will be called by the GC as well, but a struct should properly handle destroying the .init value.

-Steve

September 21, 2021

On Monday, 20 September 2021 at 18:13:53 UTC, Steven Schveighoffer wrote:

>

On 9/20/21 10:22 AM, Tejas wrote:

>

In case you still want to delete stuff deterministically despite what Steve said, I suggest you make your struct a reference and use core.memory.__delete(not recommended to use this carelessly, btw)

Do not call __delete here, use destroy. __delete will attempt to deallocate the block, which likely will fail since the key comes before the value, and GC.free on an interior pointer (I think) fails.

But if it succeeded, it would not be good. This leaves a dangling pointer inside the AA. Rehashing the AA likely would result in a memory corruption.

If you use destroy, the destructor will be called by the GC as well, but a struct should properly handle destroying the .init value.

-Steve

It doesn't succeed when object is stack allocated, otherwise it works; that's why I suggested changing S to S*.

As for why I didn't use destroy, I assumed OP wanted to free the memory. If they just want the destructor to run, then destroy is better(and safer).

(Also, hashOf seems to work just fine even after using __delete, not that it means using __delete should ever be recommended over using destroy)

import std.stdio;
import core.memory:__delete;

struct S
{
	int a;
	this(int param){
		a = param;
	}
	~this()
	{
	    writeln("S(",a,") is being destructed");
	}
}

void absoluteRemove(AA, keyType)(AA assArray, keyType key){
	//assArray.remove(key);
	__delete(assArray[key]);
}


void main()
{
	S*[int] aa;
        aa[1]  = new S(5);
        absoluteRemove(aa, 1);
        writeln(aa.hashOf);
        aa[2] = new S(10);
        writeln(aa.hashOf);
        absoluteRemove(aa, 2);
        writeln(aa.hashOf);
        writeln("Why no dtor call on remove?");
}



Output:
S(5) is being destructed
2451737883
5104465521
S(10) is being destructed
5378492086
Why no dtor call on remove?
September 21, 2021

On 9/21/21 2:06 AM, Tejas wrote:

>

On Monday, 20 September 2021 at 18:13:53 UTC, Steven Schveighoffer wrote:

>

On 9/20/21 10:22 AM, Tejas wrote:

>

In case you still want to delete stuff deterministically despite what Steve said, I suggest you make your struct a reference and use core.memory.__delete(not recommended to use this carelessly, btw)

Do not call __delete here, use destroy. __delete will attempt to deallocate the block, which likely will fail since the key comes before the value, and GC.free on an interior pointer (I think) fails.

But if it succeeded, it would not be good. This leaves a dangling pointer inside the AA. Rehashing the AA likely would result in a memory corruption.

If you use destroy, the destructor will be called by the GC as well, but a struct should properly handle destroying the .init value.

It doesn't succeed when object is stack allocated, otherwise it works; that's why I suggested changing S to S*.

Oh! I missed that subtle change. I thought you were deleting a pointer to S that lives in a S[int].

I still recommend against this because you can easily get a dangling pointer that way. Aside from that, if you are careful enough not to store a retrieved S* from the aa, you could do that. It will incur one extra allocation per element, which is not ideal.

Using destroy should be the most effective and safest mechanism.

-Steve