March 16, 2021
On Tuesday, 16 March 2021 at 08:22:07 UTC, Walter Bright wrote:
> On 3/15/2021 1:09 PM, tsbockman wrote:
>> I was trying to figure out what your original, general-purpose move *assignment* algorithm could be. That's what my code example is for.
>
> The answer is, it depends on how the programmer set up the contents of the object, in particular, how the ownership works.
> It's up to the programmer to make it work so the user of the type sees the semantics as described.

The semantics as described in the DIP are unsound. You've left out some implied extra step or assumption.

> For example, if it's pointing to unique objects, then the destructor for the destination is called first. If it's pointing to ref counted objects, then the destructor is called last.

When the source and destination refer to the same object, that object still ends up either destroyed, or in an invalid state, regardless of which order a single move and destruction are performed.

You stated earlier that making such self-moves work correctly is the specific motivation for the current wording in the DIP, but it clearly doesn't do that in either of your examples.

I've tested this with actual runnable code, so please stop responding like I just don't get it, unless you're going to critique the code example itself.

> The language doesn't specify that. The programmer does, and the programmer needs to make it work. Hence the "as if" rule.

An algorithm that works "as if" it prematurely destroys the object in self-moves, must still prematurely destroy the object in self moves. The "as if" rule allows different implementations, not different results.

You just have different results in mind from what your wrote in the DIP.
March 16, 2021
On Monday, 15 March 2021 at 07:48:04 UTC, Walter Bright wrote:
> Both the move constructor and the destructor are in the same struct, and should be developed at the same time. At some point, the program has to rely on the programmer knowing what he's doing when doing storage management.

That is a notoriously difficult thing to get right, and why the mechanism of constructor and destructor has been invented in the first place. Consider for instance that destructor design also destructs each field of the struct, in addition of running the user's code. One could argue that the the developer could destruct all fields manually, and in fact, it is certainly possible to do so. But the design chosen doesn't do that because we know ahead of time what would happen: leakage galore.

One way to think of it is in term of solidity. If I change something at point A, will something else break unexpectedly in a subtle way at point B? If yes, then the design is not solid.

When a new member is added to a struct, then this member will be destroyed automatically too, just like the others. This is correct by design. This is solid. And this is composable as the destruction of each fields simply work and the code that has to be written is limited to ensuring some cross fields invariants remains true.

If one uses the exemple of a RC smart pointer for instance, the smart pointer does not need to know how to destroy its payload, the payload knows this. The RC simply keep the RC count up to date and chooses to destroy or not its payload based on this, ie it maintains the invariant between the state of the RC and the state of the payload.

If no such invariant needs to be maintained, then the destructor can remain empty and everything works fine.

Now what happens with move? Well,t he natural way to transpose the described design to move is as follow:
1/ move all fields one by one
2/ call the move constructor on the result to maintain invariants if need be.

To me, 2/ furiously sounds like a postblit, but I'm open to the fact that alternatives. I however know for a fact that the proposed magic will open a bag of worm because it doesn't go with the same set of design principle as the rest of the contruction/destruction business.

For instance, if a field were to be added to a struct, then immediately the move assignment becomes invalid, silently. Worse, if the field itself contains something detructible, now there is something seriously wrong, potentially outside of the struct I'm working with. For instance, if that new field is a smart pointer, then the guarantee provided y the smart pointer are broken, silently.

Now we might decide, instead, that all field are going to be destroyed at the end of the move assign in the move struct, inc are there are leftovers. But we are now back to the situation where all struct MUST have a null state, or you won't be able to have them as fields of other structs.

Or we break the guarantees provided by the ctor/dtor mechanism, but in this case, why have it at all? The whole point of ctor and dtor is to ensure that invariant are kept within the program. Let's not break this invariant.

March 16, 2021
On Tuesday, 16 March 2021 at 09:14:58 UTC, Walter Bright wrote:
> Postblit's problems arose from it not having access to both objects. The opAssign does have access to both, and the qualifiers can be applied to both parameters, so I don't see a barrier to it working.
>

YES!

This is why it is unsuitable for copies. During a copy, there are 2 objects.

Any solution, that provide to objects when moving, a situation where only one object exists, will just open a can of worm of the same nature as postblit for copies opened.

This is self evident. This is so obvious that I don't know how to unpack it any further.

Pretend you have one object when you have two => problems.
Pretend you have two objects when you have one => problems.

> An opAssign gives the implementer complete control over the operation of it, including when and how destruction takes place of the original destination's contents.

That break all the invariant provided by the ctor/dtor mechanism and because struct are composable (you can use structs as member of structs) then the mess is not bounded to the one struct you are toying with.
March 16, 2021
On Thursday, 11 March 2021 at 03:33:31 UTC, Walter Bright wrote:
> The thing about "destroy after move" is to deal with the case of both the source and the destination referring to the same object. The concern is that destroying the destination's original contents first will destroy them for the source before it gets moved in.

I thought of a much simpler way to demonstrate why the DIP's proposed move-assignment semantics are unsound:

> After the move is complete, the destructor is called on the
> original contents of the constructed object. The argument is
> invalid after this move, and is not destructed.

In the self-move case, "the original contents of the constructed object", "the argument", and the destination are all the same thing. So, the DIP requires this one object to be:

1) valid (because it is the destination)
2) "invalid" (because it is "the argument")
3) "not destructed" (because it is "the argument"), and
4) destructed (because it is "the original contents of the constructed object")

This is a severe contradiction that needs to be addressed, not dismissed with the magic words "as if".

The simple solution is to formally require that self-moves have no effect.
March 17, 2021
On 16.03.21 23:30, deadalnix wrote:
> On Tuesday, 16 March 2021 at 09:14:58 UTC, Walter Bright wrote:
>> Postblit's problems arose from it not having access to both objects. The opAssign does have access to both, and the qualifiers can be applied to both parameters, so I don't see a barrier to it working.
>>
> 
> YES!
> 
> This is why it is unsuitable for copies. During a copy, there are 2 objects.
> 
> Any solution, that provide to objects when moving, a situation where only one object exists, will just open a can of worm of the same nature as postblit for copies opened.
> 
> This is self evident. This is so obvious that I don't know how to unpack it any further.
> 
> Pretend you have one object when you have two => problems.
> Pretend you have two objects when you have one => problems.
> 
>> An opAssign gives the implementer complete control over the operation of it, including when and how destruction takes place of the original destination's contents.
> 
> That break all the invariant provided by the ctor/dtor mechanism and because struct are composable (you can use structs as member of structs) then the mess is not bounded to the one struct you are toying with.

This reasoning makes sense to me, but perhaps sometimes you don't want to actually destroy the move target. E.g., it's possible that you want to move a dynamic array element-wise instead of by reference to avoid creating dangling references (if you had pointers to the array elements). There's no good reason why that should be possible for static array members but not dynamic array members. This is what Walter means by complete control. So a "better" solution would allow for this while still doing the right thing for new fields by default.

For the common case, I think the move opAssign should be auto-generated from the destructor and the move constructor/postblit. So whatever the solution is, having a move constructor/postblit should not require an implementation of the move opAssign, but it should rather auto-generate it using the move constructor/postblit.

So I think the reasoning may not fully apply to move opAssign. OTOH, I think you are making a very strong point that a (move) postblit is in the common case better than a move _constructor_. Postblit for copies is bad, but for moves into destructed/uninitialized memory, it is often precisely what we want. I guess the main benefit of a move constructor is that it allows you to fix internal references, while you are out of luck with postblit.

Summary:

move constructor:
move into uninitialized memory, error prone, internal references supported

(move) postblit:
move into uninitialized memory, composes well, internal references not supported

move opAssign: move into existing memory, error prone, allows for full control but can usually be auto-generated from destructor and move constructor/postblit


So, obvious question: What design satisfies the following constraints?

move constructor v2:
move into uninitialized memory, composes well, internal references supported

move opAssign v2:
move into existing memory, composes well, allows for full control, but can be auto-generated in the common case


It's pretty clear what a move constructor v2 would be: Require all fields to be explicitly initialized, even those that have a field initializer. This would allow to a manual postblit-like implementation, i.e., first this.tupleof=move(other.tupleof), then do fixup, as well as repairing internal references if that's required.

Issues with this:
- It does not really make sense to reinitialize fields of immutable objects that have a field initializer.
- You can't truly get postblit behavior as IIRC std.algorithm.move actually writes the init value over its argument, so maybe still supporting postblit is better. (Unless the optimizer is reliably good enough here.)


What's a move opAssign v2? Maybe move opAssign but require all fields to be mentioned?


One issue with original DIP that occurred to me while writing this stuff down:
- How do you manually move an object field-wise in a move constructor or move opAssign? You'd want to move out the fields, but the compiler does not really have a good way to see that. If you use std.algorithm.move, you get additional overhead that a direct call to the move constructor or move opAssign would not suffer. Do we rely on the optimizer here?
March 17, 2021
On Friday, 5 March 2021 at 12:19:54 UTC, Mike Parker wrote:
> This is the discussion thread for the first round of Community Review of DIP 1040, "Copying, Moving, and Forwarding":
>
> https://github.com/dlang/DIPs/blob/a9c553b0dbab1c2983a801b5e89b51c5c33d5180/DIPs/DIP1040.md



Hello,

I like this dip but, must be move/copy methods ctors? Because there need to be distinctions between copy/move ctors and other ctors, all copy/move ctors are non templates and that make some problems.

Something like opMoveCtor and opCopyCtor are easier differentiate from others ctors and can be template:

void opMoveCtor(T, this This)(T rhs){  //traits like hasMoveConstructor can work without instantion of opMoveCtor

}

instead of

this(typeof(this) rhs){/*...*/}
this(const typeof(this) rhs)const {/*...*/}
this(immutable typeof(this) rhs)immutable {/*...*/}

//and all other combination including inout and sometimes shared.

March 17, 2021
On Wednesday, 17 March 2021 at 06:35:16 UTC, vitamin wrote:
> On Friday, 5 March 2021 at 12:19:54 UTC, Mike Parker wrote:
>> This is the discussion thread for the first round of Community Review of DIP 1040, "Copying, Moving, and Forwarding":
>>
>> https://github.com/dlang/DIPs/blob/a9c553b0dbab1c2983a801b5e89b51c5c33d5180/DIPs/DIP1040.md
>
>
>
> Hello,
>
> I like this dip but, must be move/copy methods ctors? Because there need to be distinctions between copy/move ctors and other ctors, all copy/move ctors are non templates and that make some problems.
>
> Something like opMoveCtor and opCopyCtor are easier differentiate from others ctors and can be template:
>
> void opMoveCtor(T, this This)(T rhs){  //traits like hasMoveConstructor can work without instantion of opMoveCtor
>
> }
>
> instead of
>
> this(typeof(this) rhs){/*...*/}
> this(const typeof(this) rhs)const {/*...*/}
> this(immutable typeof(this) rhs)immutable {/*...*/}
>
> //and all other combination including inout and sometimes shared.

Some thoughts: ignore the implementation for now, but think about how the method based approach would change the language specification - the whole thesis of this particular DIP is (beyond move semantics themselves) to make move semantics (in a sense) more natural than the C++ solution, whereas with a method the decision to pass-by-move is now performed based on the presence of specific template arg to the operator.

Also it's a constructor, so why not call it one as we do now.

And for the specialisations, when you need to do these for the most part you end up doing them explicitly anyway (unless you are DbI-ing like a madman I guess).

Anyway those are my slightly rambling thoughts for now, more coming later.


March 17, 2021
On Wednesday, 17 March 2021 at 06:51:54 UTC, Max Haughton wrote:
> On Wednesday, 17 March 2021 at 06:35:16 UTC, vitamin wrote:
>> [...]
>
> Some thoughts: ignore the implementation for now, but think about how the method based approach would change the language specification - the whole thesis of this particular DIP is (beyond move semantics themselves) to make move semantics (in a sense) more natural than the C++ solution, whereas with a method the decision to pass-by-move is now performed based on the presence of specific template arg to the operator.
>
> [...]


Only thing I want is possibility to create template move/ctor. All other things stay same as in dip.

March 17, 2021
On Thursday, 11 March 2021 at 03:33:31 UTC, Walter Bright wrote:
> On 3/10/2021 5:27 PM, tsbockman wrote:
>> On Thursday, 11 March 2021 at 00:31:01 UTC, deadalnix wrote:
>>> On Wednesday, 10 March 2021 at 22:51:58 UTC, tsbockman wrote:
>>>> Am I the only one who thinks that it would be better to have syntax that accurately reflects the semantics, instead of just documenting "this syntax is a lie"?
>>>
>>> No, I think there is a problem with using opAssign here, because "this" will refers to something that is possibly uninitialized, and the old value may not be consumed fully.
>>>
>>> Due to problem #1, this kinda need to be a constructor rather than an opAssign.
>
> opAssign is only for assigning to initialized objects. Constructors are for uninitialized objects.
>
>
>> Yeah, studying the DIP I can't figure out what problem the move `opAssign` is supposed to solve that the constructor doesn't:
>> https://forum.dlang.org/post/kzgybicwqwlfyiiefucc@forum.dlang.org
>
> The thing about "destroy after move" is to deal with the case of both the source and the destination referring to the same object. The concern is that destroying the destination's original contents first will destroy them for the source before it gets moved in.
>
> It's the same problem "swap" has. It's also necessary semantics for a reference counted object.

It might be a stupid question, but why have move assignment in the first place? In C++, there's the copy-and-swap idiom[1]. Maybe it's obvious why it does not apply in D, but if using a swap function makes implementing a copy assignment and move assignment trivial, why not requiring opSwap instead of opAssign for an elaborate move object?

Basically, opSwap takes a typeof(this) lvalue (by reference), well, swaps contents with `this`. Usually, this means swapping all member variables (can be auto-generated easily). Then,

    ref typeof(this) opAssign(typeof(this) rhs)
    {
        this.opSwap(rhs);
        return this;
    }

does the deed. Note that the call to opAssign is to be treated like any old member function call. If the argument is an rvalue (or by the DIP the last use of an lvalue), the move constructor is used to initialize the parameter. Otherwise the copy constructor is used to initialize the parameter.

This is not a "let's do it like C++ guides" but rather "let's not repeat the mistakes C++ made". Because in C++, if one don't know copy-and-swap, one's copy/move assignment operator is probably worse than the copy-and-swap one.

Note that in C++, too, copy-and-swap only applies to elaborate move objects.

[1] https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom
March 18, 2021
On 3/16/2021 3:30 PM, deadalnix wrote:
> This is self evident. This is so obvious that I don't know how to unpack it any further.

I'm sorry, I just don't understand your objection.