3 days ago

On Wednesday, 2 October 2024 at 18:00:29 UTC, Walter Bright wrote:

>

On 10/1/2024 1:06 PM, Timon Gehr wrote:

>

[...]

The major difference between @move and __rvalue is the former is attached to the parameter, and the latter is attached to the argument. This might seem a distinction without a difference, but this has large implications with how overloading works.

[...]

The intrinsic is clearly best

struct S {
    this(ref S) { /* Copy */ }
    this(S)     { /* Move */ }
}

void main() {
    S t;
    S s1 = t;            // Copy
    S s2 = __rvalue(t);  // Move
3 days ago
On Thu, 3 Oct 2024 at 03:21, Walter Bright via Digitalmars-d < digitalmars-d@puremagic.com> wrote:

> On 10/1/2024 1:15 PM, Timon Gehr wrote:
> > I guess the new implementation you have in mind is something like the
> following?
> >
> > ```d
> > auto move(T)(ref T arg)=>__rvalue(arg);
> > ```
>
> Not exactly. __rvalue would also convert an rvalue to an rvalue.
>

-preview=rvaluerefparams addresses this; it allows an rvalue to be supplied to the lvalue there. It's actually an essential mechanic to this whole thing, because it will allow the appropriate selection of copy/move overloads where a type may define either one, or both. If a copy and/or move constructor exists, it needs to select the proper one, and the -preview handles that properly as is.


3 days ago
On Thu, 3 Oct 2024 at 04:06, Walter Bright via Digitalmars-d < digitalmars-d@puremagic.com> wrote:

> On 10/1/2024 1:06 PM, Timon Gehr wrote:
> > I think in case we did go the function route, I think any implementation
> of
> > `move` that is much more complex than the following is a failure:
> >
> > ```d
> > auto move(T)(@moved T arg)=>arg;
> > ```
>
> The major difference between @move and __rvalue is the former is attached
> to the
> parameter, and the latter is attached to the argument. This might seem a
> distinction without a difference, but this has large implications with how
> overloading works.
>
> For example, how do we distinguish a move constructor from a copy
> constructor?
> ```
> this(ref S); // copy constructor
> this(S);     // move constructor
> S s = t;     // calls copy constructor (lvalue)
> S s = f();   // calls move constructor (rvalue)
> ```
> The current overloading rules work out of the box, an rvalue goes for the
> move
> constructor, and an lvalue goes to the copy constructor.
>
> The problem here is when I want to move t into s. How do I get it to call
> the
> move constructor?
> ```
> S move(ref S s) { return s; } // convert argument from lvalue to rvalue
> S s = move(t);
> ```
> This works, however, it creates a copy of s and then moves the copy! There
> needs
> to be a way to tell the compiler to use the move construct, hence:
> ```
> S s = __rvalue(t);
> ```
> All __rvalue does is flag the expression in ( ) as an rvalue. Then the
> rest of
> the semantics go from there. Note that a struct parameter with a move
> constructor will always pass by ref regardless, which is what we want
> here.
> Also, move semantics will only work on structs. Not classes, integers,
> pointers,
> arrays, etc. If move semantics are desired for them, they'll need to be
> wrapped
> in a struct, or use a template to wrap it for you.
>
> Consider:
> ```
> this(ref S);   // copy constructor
> this(@move S); // move constructor
> ```
> I don't know how to make overloading work with this.
>

It's always some weird little detail that changes when I feel like we
discussed/designed a thing and then you implement it! :P
your __rvalue() is essentially the `T move(ref T)` intrinsic we discussed
at length; so why not just make the move intrinsic rather than this thing?
It seems trivial, but the reason I say this (as we discussed at length), is
that in addition to the trivial act of stripping away the ref to make it an
rvalue, the move expression will inevitably need to be enhanced with
lifetime tracking semantics. In the future, you will want to put logic in
that intrinsic to mark the end of lifetime of the memory region, and
possibly mark the transition of ownership for ownership tracking... 'move'
is the operation we seek, and it has lifetime related semantics involved
with the operation. __rvalue() doesn't feel like the right abstraction for
that set of semantics; you're gonna find yourself wondering where to pin
the lifetime semantics in the future...

Timon: Do you have any comment? Am I wrong?


3 days ago
On 10/3/24 01:07, Manu wrote:
> On Thu, 3 Oct 2024 at 03:21, Walter Bright via Digitalmars-d <digitalmars-d@puremagic.com <mailto:digitalmars-d@puremagic.com>> wrote:
> 
>     On 10/1/2024 1:15 PM, Timon Gehr wrote:
>      > I guess the new implementation you have in mind is something like
>     the following?
>      >
>      > ```d
>      > auto move(T)(ref T arg)=>__rvalue(arg);
>      > ```
> 
>     Not exactly. __rvalue would also convert an rvalue to an rvalue.
> 
> 
> -preview=rvaluerefparams addresses this; it allows an rvalue to be supplied to the lvalue there. It's actually an essential mechanic to this whole thing, because it will allow the appropriate selection of copy/move overloads where a type may define either one, or both. If a copy and/or move constructor exists, it needs to select the proper one, and the -preview handles that properly as is.

`-preview=rvaluerefparam` allows you to treat an rvalue as an lvalue implicitly in some contexts.

`__rvalue` allows you to treat an lvalue explicitly as an rvalue, so it will be moved.

I fail to see how those are related.
3 days ago
On 10/3/24 01:17, Manu wrote:
> On Thu, 3 Oct 2024 at 04:06, Walter Bright via Digitalmars-d <digitalmars-d@puremagic.com <mailto:digitalmars-d@puremagic.com>> wrote:
> 
>     On 10/1/2024 1:06 PM, Timon Gehr wrote:
>      > I think in case we did go the function route, I think any
>     implementation of
>      > `move` that is much more complex than the following is a failure:
>      >
>      > ```d
>      > auto move(T)(@moved T arg)=>arg;
>      > ```
> 
>     The major difference between @move and __rvalue is the former is
>     attached to the
>     parameter, and the latter is attached to the argument. This might
>     seem a
>     distinction without a difference, but this has large implications
>     with how
>     overloading works.
> 
>     For example, how do we distinguish a move constructor from a copy
>     constructor?
>     ```
>     this(ref S); // copy constructor
>     this(S);     // move constructor
>     S s = t;     // calls copy constructor (lvalue)
>     S s = f();   // calls move constructor (rvalue)
>     ```
>     The current overloading rules work out of the box, an rvalue goes
>     for the move
>     constructor, and an lvalue goes to the copy constructor.
> 
>     The problem here is when I want to move t into s. How do I get it to
>     call the
>     move constructor?
>     ```
>     S move(ref S s) { return s; } // convert argument from lvalue to rvalue
>     S s = move(t);
>     ```
>     This works, however, it creates a copy of s and then moves the copy!
>     There needs
>     to be a way to tell the compiler to use the move construct, hence:
>     ```
>     S s = __rvalue(t);
>     ```
>     All __rvalue does is flag the expression in ( ) as an rvalue. Then
>     the rest of
>     the semantics go from there. Note that a struct parameter with a move
>     constructor will always pass by ref regardless, which is what we
>     want here.
>     Also, move semantics will only work on structs. Not classes,
>     integers, pointers,
>     arrays, etc. If move semantics are desired for them, they'll need to
>     be wrapped
>     in a struct, or use a template to wrap it for you.
> 
>     Consider:
>     ```
>     this(ref S);   // copy constructor
>     this(@move S); // move constructor
>     ```
>     I don't know how to make overloading work with this.
> 
> 
> It's always some weird little detail that changes when I feel like we discussed/designed a thing and then you implement it! :P
> your __rvalue() is essentially the `T move(ref T)` intrinsic we discussed at length; so why not just make the move intrinsic rather than this thing? It seems trivial, but the reason I say this (as we discussed at length), is that in addition to the trivial act of stripping away the ref to make it an rvalue, the move expression will inevitably need to be enhanced with lifetime tracking semantics. In the future, you will want to put logic in that intrinsic to mark the end of lifetime of the memory region, and possibly mark the transition of ownership for ownership tracking... 'move' is the operation we seek, and it has lifetime related semantics involved with the operation. __rvalue() doesn't feel like the right abstraction for that set of semantics; you're gonna find yourself wondering where to pin the lifetime semantics in the future...
> 
> Timon: Do you have any comment? Am I wrong?

I don't think you are wrong, there are just a few different ways to go about this and to me it seems Walter opened a discussion to explore the design space a bit further.

I think `__rvalue` indeed boils down to a `move` intrinsic, just by a less nice name.

As far as I understand, Walter's current intention is to keep the memory region alive by default after a move, where killing the memory region is a potential optimization enabled by alias analysis.

In any case, I think if it is not possible to implement a `move` function that internally uses `__rvalue` and behaves just like `__rvalue` from the outside, that would indicate an issue with the type system and it in particular precludes perfect forwarding for moves without unnecessary intermediate memory operations and move constructor calls. This is why I also discuss enabling the functionality via parameter attributes instead.
3 days ago
On Thu, 3 Oct 2024 at 10:06, Timon Gehr via Digitalmars-d < digitalmars-d@puremagic.com> wrote:

> On 10/3/24 01:07, Manu wrote:
> > On Thu, 3 Oct 2024 at 03:21, Walter Bright via Digitalmars-d <digitalmars-d@puremagic.com <mailto:digitalmars-d@puremagic.com>>
> wrote:
> >
> >     On 10/1/2024 1:15 PM, Timon Gehr wrote:
> >      > I guess the new implementation you have in mind is something like
> >     the following?
> >      >
> >      > ```d
> >      > auto move(T)(ref T arg)=>__rvalue(arg);
> >      > ```
> >
> >     Not exactly. __rvalue would also convert an rvalue to an rvalue.
> >
> >
> > -preview=rvaluerefparams addresses this; it allows an rvalue to be supplied to the lvalue there. It's actually an essential mechanic to this whole thing, because it will allow the appropriate selection of copy/move overloads where a type may define either one, or both. If a copy and/or move constructor exists, it needs to select the proper one, and the -preview handles that properly as is.
>
> `-preview=rvaluerefparam` allows you to treat an rvalue as an lvalue implicitly in some contexts.
>
> `__rvalue` allows you to treat an lvalue explicitly as an rvalue, so it will be moved.
>
> I fail to see how those are related.
>

__rvalue() needs to be callable with an lvalue or an rvalue.
That's the only specific interaction on this matter, but beyond that with
respect to move semantics in practice; in your application, you will
encounter types with copy and/or move constructors/assign operators,
general overloads, etc... you need to be able to pass the values that you
have and your code needs to work without friction.
If there is a copy constructor and no move constructor for instance (very
common situation) and you pass an rvalue, the code needs to compile. Later
if you add a move constructor, it will automatically select that as the
appropriate choice.


3 days ago
On 10/3/24 02:22, Manu wrote:
> On Thu, 3 Oct 2024 at 10:06, Timon Gehr via Digitalmars-d <digitalmars- d@puremagic.com <mailto:digitalmars-d@puremagic.com>> wrote:
> 
>     On 10/3/24 01:07, Manu wrote:
>      > On Thu, 3 Oct 2024 at 03:21, Walter Bright via Digitalmars-d
>      > <digitalmars-d@puremagic.com <mailto:digitalmars-d@puremagic.com>
>     <mailto:digitalmars-d@puremagic.com <mailto:digitalmars-
>     d@puremagic.com>>> wrote:
>      >
>      >     On 10/1/2024 1:15 PM, Timon Gehr wrote:
>      >      > I guess the new implementation you have in mind is
>     something like
>      >     the following?
>      >      >
>      >      > ```d
>      >      > auto move(T)(ref T arg)=>__rvalue(arg);
>      >      > ```
>      >
>      >     Not exactly. __rvalue would also convert an rvalue to an rvalue.
>      >
>      >
>      > -preview=rvaluerefparams addresses this; it allows an rvalue to be
>      > supplied to the lvalue there. It's actually an essential mechanic to
>      > this whole thing, because it will allow the appropriate selection of
>      > copy/move overloads where a type may define either one, or both.
>     If a
>      > copy and/or move constructor exists, it needs to select the
>     proper one,
>      > and the -preview handles that properly as is.
> 
>     `-preview=rvaluerefparam` allows you to treat an rvalue as an lvalue
>     implicitly in some contexts.
> 
>     `__rvalue` allows you to treat an lvalue explicitly as an rvalue, so it
>     will be moved.
> 
>     I fail to see how those are related.
> 
> 
> __rvalue() needs to be callable with an lvalue or an rvalue.
> That's the only specific interaction on this matter, but beyond that with respect to move semantics in practice; in your application, you will encounter types with copy and/or move constructors/assign operators, general overloads, etc... you need to be able to pass the values that you have and your code needs to work without friction.
> If there is a copy constructor and no move constructor for instance (very common situation) and you pass an rvalue, the code needs to compile. Later if you add a move constructor, it will automatically select that as the appropriate choice.

Well, I think you can interpret this as overload resolution between a `ref` and non-`ref` overload as well, which is a bit more powerful because the callee can actually determine which one it was. Don't get me wrong, I like `-preview=rvaluerefparam`, but I don't think it helps or hurts us here.
3 days ago
On 10/3/24 02:42, Timon Gehr wrote:
> Later if you add a move constructor, it will automatically select that as the appropriate choice.

About this, actually it might not do so with `-preview=rvaluerefparam` sometimes because the rvalue-ness of the argument is not known statically.

Basically, let's assume S has both a copy constructor and a move constructor

```d
void byref(ref S s){ // ABI: passed by reference, caller cleanup
    S t = s; // calls copy constructor
    // ...
}
```

```d
void byval(S s){ // ABI: passed by value, callee cleanup
    S t = s; // calls move constructor (with DIP1040)
    // ...
}
```

```d
void bymove(@move S s){ // ABI: passed by reference, callee cleanup
    S t = s; // calls move constructor (with DIP1040)
    // ...
}
```

```d
byref(s); // passes &s, move constructor called 0 times overall

byval(move(s)); // moves s, has to call move constructor, move constructor called twice overall

bymove(s); // semantically moves, actually passes &s. move constructor is only called once, in the function body
```

Of course, you can do:

```d
void explicitmove(ref S s){
    S t = move(s);
}
```

That would have similar semantics in practice as the `bymove` (though the caller will always call the destructor with `explicitmove`, while with `bymove`, the caller may be able to elide it). However, you had to be explicit about it. It is not true that the code is upgraded for free, because `byref` actually assumes that the caller is responsible for cleaning up the parameter. This is true whether it is an rvalue or an lvalue.

3 days ago
On 10/3/24 02:57, Timon Gehr wrote:
> On 10/3/24 02:42, Timon Gehr wrote:
>> Later if you add a move constructor, it will automatically select that as the appropriate choice. 

(Sry, Thunderbird did not properly cite this, this was written by Manu, not me.)
3 days ago
One thing that is a bit annoying is:

```d
void byval(S s);
void bymove(@move S s);

byval(move(s)); // has to move on call, caller can in principle elide the .init blit

bymove(s); // does not have to move on call, but callee has to leave argument in `.init` state and this reinitialization cannot be elided without inlining
```

Of course, it would be even better if `.init` were not required at all...