October 16 Re: Move Constructor Syntax | ||||
---|---|---|---|---|
| ||||
Posted in reply to ShadoLight | On 16.10.24 15:09, ShadoLight wrote:
>> That would make sense, but this would in turn mean that the move constructor can never be invoked explicitly.
>
> What do you mean?
> s3 = S(S(2));
> ...is invoking the move constructor explicitly.
I meant the original syntax before the lowering:
s3 = S(s2)
IIRC, you say that the compiler would lower it to first a copy constructor to create a temporary, then a move constructor on the temporary. Thus, seen from the outside, it would keep the current behavior (I mean, s2 would still be valid).
But what if I did want to move s2 into s3? How would I do it? Or is a move something that cannot be forced explicitly? I didn't see this addressed in the DIP either.
The problem I see is that you can't have it both ways with the proposed syntax: either you can't call a move constructor manually on an lvalue because an intermediate copy gets inserted, or you are changing the semantics for the existing usages.
Again, perhaps move constructors are not meant to be invoked directly, but I think the DIP should be explicit about what happens in that case, or state that it's not allowed.
|
October 16 Re: Move Constructor Syntax | ||||
---|---|---|---|---|
| ||||
Posted in reply to Timon Gehr | On 16.10.24 15:06, Timon Gehr wrote: > With Walter's current preferred design, a new dummy object is created in the old memory location. I.e., it has to be valid, but it does not have to contain any data. [...] > A key difference between `this(S s)` and `=this(ref S)` is that the latter elides the destructor by default, while for the former, the least surprising semantics would be that it calls the destructor of the source at the end of the scope by default. There would then need to be an additional feature to elide the destructor call with manual syntax. Let me go back to my original example, slightly modified to make my point more clear: ```d struct S { int i; this(C)(C c) if (is(C : int)) { this.i = c; } alias i this; } void main() { S s1, s2, s3; int i = 1; s1 = S(1); s2 = S(i); s3 = S(s2); // This was most certainly not intended as a move constructor. assert(s2.i == 1); assert(s3.i == 1); } ``` Would this code still be guaranteed to pass the asserts if the signature for move constructors becomes `this (S s)` (Walter's proposal)? I mean based on assurances by the language itself, not on what the compiler might decide to do (or not) with s2. |
October 16 Re: Move Constructor Syntax | ||||
---|---|---|---|---|
| ||||
Posted in reply to Arafel | On Wednesday, 16 October 2024 at 13:23:53 UTC, Arafel wrote: > On 16.10.24 15:09, ShadoLight wrote: >>> That would make sense, but this would in turn mean that the move constructor can never be invoked explicitly. >> >> What do you mean? >> s3 = S(S(2)); >> ...is invoking the move constructor explicitly. > > I meant the original syntax before the lowering: > > s3 = S(s2) > No, I think we are missing each other. Maybe the examples are a bit confusing because of the variable naming. Let's rename them: Something like: ```d S a, b, c; a = S(1); // case(1) -> this (int i) b = S(a); // case(2) -> this (ref S s) -> implicit copy ctor c = S(S(2)); // case(3) -> this (S s) // ...and a is valid here ``` > IIRC, you say that the compiler would lower it to first a copy constructor to create a temporary, then a move constructor on the temporary. Thus, seen from the outside, it would keep the current behavior (I mean, s2 would still be valid). > I'm asking if, in the case where the compiler sees a lvalue being used for construction, but with no copy constructor present (like in your examples) - it is feasible if the compiler creates a copy constructor for you (like in C++)? So no temporary is created in case(2), since this (implicit) copy constructor will be invoked, and normal copy construction proceeds. This also guarantees `a` (in my example, or `s1` in your example) remains valid after the constructor. This is simple and matches what Manu proposes. case(3 ) `c = S(S(2));` on the other hand passes a rvalue instance of S(2) to the move constructor: - the `this (int i)` constructor is first called and a temporary S rvalue is created, - then the temporary rvalue is passsed to `this (S s)` move constructor and 'moved' as per the DIP. > But what if I did want to move s2 into s3? How would I do it? Or is a move something that cannot be forced explicitly? I didn't see this addressed in the DIP either. > In this case we can simply treat moving an existing `s2` into `s3` as ambiguous (because you cannot, if you want to maintain the normal constructor syntax, differentiate a move (if that is what you want) from a copy of a `s2` lvalue into `s3`. If you just do... `b = S(a); // case(2) -> this (ref S s) -> implicit copy ctor` ... the copy constructor is always preferred. To force a move, you should need to do... `b = S(__rvalue(a));` ... to turn `a` into a rvalue, which then matches on the move constructor. I think this is reasonable and AFAICS this is in line with move semantics. I also don't think this will break code. If a copy of s2 into s3 is done instead of an expected move of s2 into s3 (and s2 is not accessed again afterwards - which is expected because why else would you prefer the move?), then s2 will either be collected by the GC or it's destructor will be called if it is on the stack and it goes out of scope. > The problem I see is that you can't have it both ways with the proposed syntax: either you can't call a move constructor manually on an lvalue because an intermediate copy gets inserted, or you are changing the semantics for the existing usages. > Right. Which is why I'm proposing that `copy` takes precedence over `move` for lvalues, and `move` takes precedence over `copy` for rvalues. This can then be used to optimize finding the best match in the overload set. I think this is in line with Manu's expectations. > Again, perhaps move constructors are not meant to be invoked directly, but I think the DIP should be explicit about what happens in that case, or state that it's not allowed. I don't see why not. Just invoke it with an rvalue. |
October 16 Re: Move Constructor Syntax | ||||
---|---|---|---|---|
| ||||
Posted in reply to ShadoLight | On Wednesday, 16 October 2024 at 15:02:46 UTC, ShadoLight wrote: > On Wednesday, 16 October 2024 at 13:23:53 UTC, Arafel wrote: >> On 16.10.24 15:09, ShadoLight wrote: >>>> That would make sense, but this would in turn mean that the move constructor can never be invoked explicitly. >>> ... > > Something like: > ```d > S a, b, c; > a = S(1); // case(1) -> this (int i) > b = S(a); // case(2) -> this (ref S s) -> implicit copy ctor > c = S(S(2)); // case(3) -> this (S s) > // ...and a is valid here > ``` > I know that `c = S(S(2));` is a bit of an silly rvalue example (just do `c = S(2);` for the same result), but it is just to show the principle. For a somewhat better example: ```d struct S { this (int i) { } //ctor this (S s) { } //move ctor //implicitly generated copy ctor if needed } struct X { static int j; static S factoryS() { S s = S(j++); return s; } } void main() { S a, b, c; a = S(1); // ctor b = S(a); // (implicit) copy ctor c = S(X.factoryS()); // move ctor } ``` |
October 16 Re: Move Constructor Syntax | ||||
---|---|---|---|---|
| ||||
Posted in reply to Arafel | On 10/16/24 15:39, Arafel wrote:
> On 16.10.24 15:06, Timon Gehr wrote:
>
>> With Walter's current preferred design, a new dummy object is created in the old memory location. I.e., it has to be valid, but it does not have to contain any data.
>
> [...]
>
>> A key difference between `this(S s)` and `=this(ref S)` is that the latter elides the destructor by default, while for the former, the least surprising semantics would be that it calls the destructor of the source at the end of the scope by default. There would then need to be an additional feature to elide the destructor call with manual syntax.
>
> Let me go back to my original example, slightly modified to make my point more clear:
>
>
> ```d
> struct S {
> int i;
> this(C)(C c) if (is(C : int)) {
> this.i = c;
> }
>
> alias i this;
> }
>
> void main() {
> S s1, s2, s3;
> int i = 1;
> s1 = S(1);
> s2 = S(i);
> s3 = S(s2); // This was most certainly not intended as a move constructor.
> assert(s2.i == 1);
> assert(s3.i == 1);
> }
> ```
>
> Would this code still be guaranteed to pass the asserts if the signature for move constructors becomes `this (S s)` (Walter's proposal)? I mean based on assurances by the language itself, not on what the compiler might decide to do (or not) with s2.
Yes, I think the sane design is that if you pass an lvalue to an rvalue parameter, that results in a copy (during argument passing), and the copy is destroyed when it goes out of scope in the constructor.
So if you explicitly call an rvalue constructor, that would behave the same as previously.
Even with the unamended DIP1040, I think assertion failures would not fire in your example. The way DIP1040 might break such code is if `S` has a destructor.
|
October 16 Re: Move Constructor Syntax | ||||
---|---|---|---|---|
| ||||
Posted in reply to Timon Gehr | On 10/16/24 20:21, Timon Gehr wrote:
>
> Yes, I think the sane design is that if you pass an lvalue to an rvalue parameter, that results in a copy (during argument passing), and the copy is destroyed when it goes out of scope in the constructor.
>
> So if you explicitly call an rvalue constructor, that would behave the same as previously.
(Of course, this is all assuming last-use analysis does not insert moves, but it cannot do that in your example because the asserts read the value after the last opportunity for a move.)
|
October 16 Re: Move Constructor Syntax | ||||
---|---|---|---|---|
| ||||
Posted in reply to Timon Gehr | On 16/10/24 20:21, Timon Gehr wrote:
> Yes, I think the sane design is that if you pass an lvalue to an rvalue parameter, that results in a copy (during argument passing), and the copy is destroyed when it goes out of scope in the constructor.
>
> So if you explicitly call an rvalue constructor, that would behave the same as previously.
>
> Even with the unamended DIP1040, I think assertion failures would not fire in your example. The way DIP1040 might break such code is if `S` has a destructor.
I guess that would be workable iff templated constructors are excluded from being considered move constructors, otherwise there could be cases where a move constructor is found that was never meant as such.
Would a way of explicitly requesting a move, like `__rvalue`, be available in case the compiler's detection mechanism of last use needs to be overridden?
All in all, I still see no such big benefit in changing the meaning of existing valid code, when adding new syntax would be much clearer all around.
Of course, it would be different if we were designing the language from zero now, or if it were part of D3... then I might even find it an elegant solution.
|
October 16 Re: Move Constructor Syntax | ||||
---|---|---|---|---|
| ||||
Posted in reply to ShadoLight | On 16/10/24 17:02, ShadoLight wrote:
> [...]
I think I agree with you then, I was missing the `__rvalue` possibility (assuming templated constructors are excluded).
However, as I mentioned in my other reply, I still don't see the big benefit of not adding new syntax compared to all the confusion it causes, including possibly breaking existing code.
I mean, I understand that the language needs to be kept clean and lean, but I would say this is one situation where having specific syntax is merited.
|
October 16 Re: Move Constructor Syntax | ||||
---|---|---|---|---|
| ||||
Posted in reply to Arafel | On 10/16/24 22:06, Arafel wrote: > On 16/10/24 20:21, Timon Gehr wrote: >> Yes, I think the sane design is that if you pass an lvalue to an rvalue parameter, that results in a copy (during argument passing), and the copy is destroyed when it goes out of scope in the constructor. >> >> So if you explicitly call an rvalue constructor, that would behave the same as previously. >> >> Even with the unamended DIP1040, I think assertion failures would not fire in your example. The way DIP1040 might break such code is if `S` has a destructor. > > I guess that would be workable iff templated constructors are excluded from being considered move constructors, otherwise there could be cases where a move constructor is found that was never meant as such. > ... I guess the question is what else could `S(s)` reasonably do for a `S s;`. Whether templated or not. > Would a way of explicitly requesting a move, like `__rvalue`, be available in case the compiler's detection mechanism of last use needs to be overridden? > ... Yes, such mechanisms would still be needed. It is an important point as it is easy to think move semantics just means move constructors, but it is more than that. Perfect forwarding for rvalues is important too. > All in all, I still see no such big benefit in changing the meaning of existing valid code, when adding new syntax would be much clearer all around. > ... I agree with Manu's reasoning why having `this(ref S)` and `this(S)` work as "initialization from lvalue" and "initialization from rvalue", corresponding to copy and move respectively would be cleaner. But overall I don't have a strong preference, as dedicated syntax also has some benefits. The thing I think is most important is getting the semantics right. |
October 17 Re: Move Constructor Syntax | ||||
---|---|---|---|---|
| ||||
Posted in reply to Timon Gehr Attachments:
| On Thu, 17 Oct 2024, 07:36 Timon Gehr via Digitalmars-d, < digitalmars-d@puremagic.com> wrote: > On 10/16/24 22:06, Arafel wrote: > > On 16/10/24 20:21, Timon Gehr wrote: > >> Yes, I think the sane design is that if you pass an lvalue to an rvalue parameter, that results in a copy (during argument passing), and the copy is destroyed when it goes out of scope in the constructor. > >> > >> So if you explicitly call an rvalue constructor, that would behave the same as previously. > >> > >> Even with the unamended DIP1040, I think assertion failures would not fire in your example. The way DIP1040 might break such code is if `S` has a destructor. > > > > I guess that would be workable iff templated constructors are excluded > > from being considered move constructors, otherwise there could be cases > > where a move constructor is found that was never meant as such. > > ... > > I guess the question is what else could `S(s)` reasonably do for a `S > s;`. Whether templated or not. > > > Would a way of explicitly requesting a move, like `__rvalue`, be > > available in case the compiler's detection mechanism of last use needs > > to be overridden? > > ... > > Yes, such mechanisms would still be needed. It is an important point as it is easy to think move semantics just means move constructors, but it is more than that. Perfect forwarding for rvalues is important too. > > > All in all, I still see no such big benefit in changing the meaning of > > existing valid code, when adding new syntax would be much clearer all > > around. > > ... > > I agree with Manu's reasoning why having `this(ref S)` and `this(S)` > work as "initialization from lvalue" and "initialization from rvalue", > corresponding to copy and move respectively would be cleaner. > > But overall I don't have a strong preference, as dedicated syntax also has some benefits. The thing I think is most important is getting the semantics right. > Special-casing the constructor is just admitting that the overload selection mechanics are broken for **all other function calls**... It's not a matter of 'preference'; if the overload mechanics work properly then special casing the constructor wouldn't even be thinkable or raised in conversation. It's a hack; a really dumb hack which just leaves EVERY OTHER FUNCTION broken. The fact you have "no strong preference" surprises me, maybe I've misunderstood some way in which this is not a hack that's totally broken? Can you explain to me how every other function call isn't broken under the special-case-for-move-constructor solution? Overload selection has to work, it is basically the meat of this whole thing... there's not really anything else to it. Broken calling semantics for every function other than the constructor is not a 'compromise', it baffles me that people would even consider it. I mean, you don't 'call' constructors so often, but you do call everything else. > |
Copyright © 1999-2021 by the D Language Foundation