Discussion Thread: DIP 1040--Copying, Moving, and Forwarding--Community Review Round 1

tsbockman thomas.bockman at gmail.com
Thu Mar 11 21:45:49 UTC 2021


On Thursday, 11 March 2021 at 08:36:18 UTC, Walter Bright wrote:
> On 3/10/2021 8:17 PM, tsbockman wrote:
>> Wouldn't it make more sense to just skip the move operation 
>> entirely when the source and destination are the same? Or are 
>> there circumstances in which that cannot be determined, even 
>> at runtime?
>
> The idea is that the move assignment operation takes care of 
> this, and makes it "as if" the move was done, then the 
> destruction.

Is it the responsibility of the explicit body of the custom move 
assignment operator to do the destruction, or the responsibility 
of the compiler to insert a call to the destructor somewhere? If 
the compiler inserts the call, is this done inside the move 
assignment operator, or at each call site?

My concern is how to make code like this work correctly:

/////////////////////////////////////////////////
module app;

ptrdiff_t netAllocCount = 0;
void* malloc(size_t size) @system nothrow @nogc {
     import core.stdc.stdlib : malloc;
     void* ptr = malloc(size);
     netAllocCount += int(ptr !is null);
     return ptr;
}
void free(void* ptr) @system nothrow @nogc {
     import core.stdc.stdlib : free;
     netAllocCount -= int(ptr !is null);
     free(ptr);
}

struct S {
private:
     struct Data {
         uint data;
         int refCount = 1;
     }

     Data* ptr;
     Data internal;

public:
     bool isUnique() const pure @safe nothrow @nogc {
         return (ptr is &internal); }
     ref inout(uint) data() return inout pure @safe nothrow @nogc {
         return ptr.data; }

     // normal construction and assignment
     this(bool unique) @trusted nothrow @nogc {
         construct(unique); }
     private void construct(bool unique) @trusted nothrow @nogc {
         pragma(inline, false); // Don't let inlining make things 
work by accident.
         if(unique) {
             ptr = &internal;
         	internal = Data.init;
         } else {
             ptr = cast(Data*) malloc(size_t.sizeof * 2);
             (*ptr) = Data.init;
         }
     }
     ref typeof(this) opAssign(bool unique) return @safe nothrow 
@nogc {
         destruct(this);
         construct(unique);
         return this;
     }

     // copy construction
     @disable this(this);
     this(ref typeof(this) source) pure @trusted nothrow @nogc {
         if(source.isUnique) {
             ptr = &internal;
             internal = source.internal;
         } else {
             ptr = source.ptr;
             ptr.refCount += 1;
         }
     }

     // move construction and assignment (using the DIP's 
misleading syntax):
     this(S source) pure @trusted nothrow @nogc {
         if(source.isUnique) { // This works, because source is 
really by reference.
             ptr = &internal;
             internal = source.internal;
         } else
             ptr = source.ptr;
     }
     void opAssign(S source) @trusted nothrow @nogc {
         // What goes here, and what is the full lowering of a 
move assignment call?
     }

     // destructor
     ~this() @safe nothrow @nogc {
         destruct(this); }
     private void destruct(ref S s) @trusted nothrow @nogc {
         pragma(inline, false); // Don't let inlining make things 
work by accident.
         if(!s.isUnique) {
             s.ptr.refCount -= 1;
             if(s.ptr.refCount <= 0)
                 free(s.ptr);
         }
         s.ptr = null;
     }
}

void main() @safe {
     import std.stdio : writeln;
     {
         S a = true, b = false, c = b;
         a.data = 1;
         b.data = 2;
         a = b;
         c.data = 3;
         assert(a.data == 3);
     }
     assert(netAllocCount == 0);
}
/////////////////////////////////////////////////

Syntax aside, I think the way it should be done is this:

// Lower the `a = b;` line in main, above, to this:
if(&b !is &a) { // Moving a variable into itself is a no-op.
     destroy(a);
     /* Just call the move constructor; there is no point
     to the move assignment operator as its body would be 
identical: */
     a.__ctor(b);
}

I've tested this algorithm, and it seems to work as intended 
(although of course it has to be expressed somewhat differently 
without the DIP). But, that's clearly not what you have in mind. 
I just can't figure out what you think the alternative is:

// Maybe this is your proposed lowering for the `a = b;` line in 
main, above?
{
     S oldDest = a; // You all say no copy constructor call is 
necessary here, but...
     a.opAssign(b); // (Body is the same as that of the move 
constructor.)
     // ...the implicit destroy(oldDest); at scope exit crashes if 
oldDest was only blit.
}

You can put the lowering logic inside of the move opAssign 
instead, but it doesn't change the fundamental problem. If no 
actual general-purpose lowering is possible that accurately 
reflects the intended semantics of, "After the move is complete, 
the destructor is called on the original contents of the 
constructed object," then the DIP's description of the semantics 
is simply incorrect and should be replaced with something more 
rigorous.


More information about the Digitalmars-d mailing list