Search code examples
dunique-ptrraiideterministicphobos

Deterministic destruction of container-owned objects (or how to put a Unique (std.typecons.Unique) into a D Phobos container)?


I'm trying to instantiate a container full of Unique resources, in an attempt to ensure that when the container is destroyed, all items managed (owned) by the container are also destroyed, automatically and right then.

The following (non-Unique) code behaves as expected. Notice the Foo objects aren't being destroyed until the app exits (the GC would have reclaimed them eventually). Ignoring the GC for the moment, by not destroying them deterministically at the time the DList is destroyed--at the "exiting scope" message--the objects in the container have effectively leaked for the duration of the app's lifetime:

import  std.stdio,
        std.container,
        std.range,
        std.typecons,
        std.random;

class Foo
{
  this()
  {
    debug( List ) writefln( "  %s constructor invoked", this.classinfo.name );
  }

  ~this()
  {
    debug( List ) writefln( "  %s destructor invoked", this.classinfo.name );
  }
}


int main( string[] args ) {
  debug( List ) writeln( "main():" );
  {
    debug( List ) writeln( "  entering scope" );
    scope auto list = DList!( Foo )();

    immutable ELEMENTS_TO_MAKE = 5;
    for( auto i = 0; i < ELEMENTS_TO_MAKE; ++i )
    {
      Foo foo = new Foo();
      list.insertBack( foo ); 
    }
    debug( List ) writefln( "  Length: %s elements in container", walkLength( list[] ) ); 
    debug( List ) writeln( "  exiting scope" );
  }
  debug( List ) writeln( "  exiting app" );
  return 0;
}

Gives the following output, as expected:

main():
  entering scope
  main.Foo constructor invoked
  main.Foo constructor invoked
  main.Foo constructor invoked
  main.Foo constructor invoked
  main.Foo constructor invoked
  Length: 5 elements in container
  exiting scope
  exiting app
  main.Foo destructor invoked
  main.Foo destructor invoked
  main.Foo destructor invoked
  main.Foo destructor invoked
  main.Foo destructor invoked

But when I update the app to work with Unique's, things fall apart:

...

int main( string[] args ) {
  debug( List ) writeln( "main():" );
  {
    debug( List ) writeln( "  entering scope" );
    scope auto list = DList!( Unique!Foo )();

    immutable ELEMENTS_TO_MAKE = 5;
    for( auto i = 0; i < ELEMENTS_TO_MAKE; ++i )
    {
      Unique!Foo foo = new Foo();
      list.insertBack( foo.release );  //looks like Phobos containers can't hold Unique's??? :( 
    }
    debug( List ) writefln( "  Length: %s elements in container", walkLength( list[] ) ); 
    debug( List ) writeln( "  exiting scope" );
  }
  debug( List ) writeln( "  exiting app" );
  return 0;
}

The above code gives the following output:

main():
  entering scope
  main.Foo constructor invoked
  main.Foo destructor invoked
Bus error: 10

Commenting out the list.insertBack() line silences the Bus error 10. Any thoughts on how I can get automatic and deterministic destruction of my container-owned objects?


Solution

  • The most major problem here is that the Phobos DList does not take ownership of the object - it uses a GC managed Node* internally, so even if you fix the null pointer problem that causes the bus error you saw, you still won't get the destruction pattern you want.

    Getting into the details: Unique is buggy, it isn't supposed to be copyable, but it is... it claims to be polymorphic, but it only minimally is, and it doesn't check for null before trying to delete its payload, which causes the bus error you see.

    Frankly, it looks wrong to me from top to bottom. My recommendation: don't use it.

    Now, if you write a more correct Unique, with @disable this(this), a null check in the destructor, etc.... you'll find it doesn't work with std.container.DList because Dlist assigns stuff internally; it doesn't use move operations. A correct Unique with Dlist doesn't compile.

    So, let's try RefCounted. Phobos' RefCounted doesn't work with classes. Fantastic. Well, we can write our own easily enough, I just did a quick and sloppy one here http://arsdnet.net/dcode/unique_refcounted.d U is a more correct Unique and RC is a more correct RefCounted.

    Now we can see the refcount changing... but never hitting zero. Why? Crack open the source code for DList and you'll see that it uses new Node internally.

    So those are owned by the GC and thus not destroyed until the collector runs anyway.


    Bottom line, you have to write your own DList container. Looks like std.container.Array actually /does/ take ownership, its destructor does indeed destroy the payloads (though it doesn't free the payload memory - so it will call the destructor.

    So if you use a more correct ref counting implementation you can use Array - the link I posted above does this so you can see that it works, minimal changes from the DList - but if you do want a linked list, you've gotta DIY and write a proper destructor there.