Move Constructors - Converting Lvalues to Rvalues
Timon Gehr
timon.gehr at gmx.ch
Wed Oct 2 18:57:24 UTC 2024
On 10/2/24 20:00, Walter Bright 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.
> ...
I don't think it affects how overloading works. You just treat it as a
by-value argument as far as overloading and lifetime handling is
concerned. In terms of ABI however, you pass the argument by reference.
Some sort of NRVO would make the occurrence of `arg` above a move.
This would be useful in general. Consider this program:
```d
import std.stdio;
struct S{
this(ref S){ writeln("copy"); }
~this(){ writeln("destructor"); }
}
S foo(S s)=>s;
void main(){
S s=foo(S());
// ...
}
```
This prints:
```
copy
destructor
destructor
```
There is actually no reason for this to make a copy, and the compiler
would in principle be able to figure it out.
> 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.
> ...
`__rvalue` makes sense and is compatible with something like `@move`.
However, I think it is in principle not needed if there is `@move`, as
an lvalue `x` that is passed to a `@move` parameter would be treated
just like `__rvalue(x)`.
> Consider:
> ```
> this(ref S); // copy constructor
> this(@move S); // move constructor
> ```
> I don't know how to make overloading work with this.
Well, those are constructors. You use one of them for copies and the
other for moves.
In general, `@move` would be incompatible with `ref`. If `ref` is
overloaded with `@move`, lvalues go to the `ref` overload and rvalues go
to the `@move` overload. (So overloading works just as if there was no
`@move` on the parameter.)
If you have an lvalue that should go to the `@move` overload, you
explicitly call `move`.
Of course, with DIP1040, in principle the last use of an lvalue could
prefer the `@move` overload instead.
Of course another question one might ask is what happens if you have:
```d
void foo(T t);
void foo(@move T t);
T t;
foo(t);
```
I think that would just be ambiguous, just as if there were no `@move`.
A benefit of explicit `@move` is that the move constructor ABI is not an
invisible special case, and is also not magic.
A drawback of explicit `@move` is that people might do:
```d
this(ref S);
this(S); // (no @move)
```
And it is a priori not so clear what this should do. Maybe it should be
disallowed, like it is now.
More information about the Digitalmars-d
mailing list