September 05, 2019
On Thursday, 5 September 2019 at 17:53:29 UTC, Suleyman wrote:
> That sounds like an optional optimization for the compiler. That's if the compiler finds that s is never used after the call to bar.

It could be, but I'm more interested in allowing this with an explicit move (incl. allowed reuse of that lvalue later). Then I don't see any reason for rvalue refs in D.

> Calling the destructor twice is not more efficient than one call to the move constructor and one call to the destructor.

C++/your current proposal ends in 2 destructions too (moved-to temporary and moved-from lvalue), so there's no overhead at all.
September 05, 2019
On Thursday, 5 September 2019 at 15:49:37 UTC, kinke wrote:
> On Thursday, 5 September 2019 at 15:32:31 UTC, Suleyman wrote:
>> Practical example:
>> [...]
>> void main()
>> {
>>     auto s = S(_1GB);
>>     bar(__move(s));  // calls move ctor
>>     assert(s.length == 0); // s is still usable
>> }
>
> Yes, that's what C++ does, and is not as efficient as can be. What I'm looking for is that there's no actual moving (move ctor call etc.) at all for move/__move in an argument expression. In your code example:
>
> void main()
> {
>     auto s = S(_1GB);
>     // does NOT call move ctor, just passes `s` by ref directly instead of a moved-to
>     // temporary
>     bar(__move(s));
>     // after the call, destruct `s` and reset to T.init
>     assert(s.length == 0); // s is still usable
>     // this now does call the move ctor:
>     auto s2 = __move(s);
> } // `s` goes out of scope and is destructed again


There's still similar to the problem we have now. You're still doing a memcpy() each time with S.init. And you are calling the destructor now too, which entirely depends on what it is.


void foo(S value, int n = 0) {

    if ( n > 32 ) {
         return;
    }

    foo( move(value), n + 1);
}


S lvalue;

foo( move(lvalue) ); // "lvalue" destructed and set to S.init 32 times

September 05, 2019
On Thursday, 5 September 2019 at 18:46:59 UTC, Exil wrote:
> There's still similar to the problem we have now. You're still doing a memcpy() each time with S.init.

Resetting the moved-from instance to T.init or something similar is what you'd do in the move ctor anyway (besides blitting the previous contents into the new instance and maybe doing some more adjustments), and definitely what the default implementation of the move ctor would do.

> And you are calling the destructor now too, which entirely depends on what it is.

As stated above, there's no extra destruction - the first one is for the by-value parameter (which would otherwise be the moved-from temporary), and the second is the regular destruction of the moved-from lvalue.
September 05, 2019
On Thursday, 5 September 2019 at 18:59:50 UTC, kinke wrote:
> On Thursday, 5 September 2019 at 18:46:59 UTC, Exil wrote:
>> There's still similar to the problem we have now. You're still doing a memcpy() each time with S.init.
>
> Resetting the moved-from instance to T.init or something similar is what you'd do in the move ctor anyway (besides blitting the previous contents into the new instance and maybe doing some more adjustments), and definitely what the default implementation of the move ctor would do.

Maybe I should put more emphasis on the advantage: saving an allocation of a temporary and move-constructing it. That's peanuts for Suleyman's struct, but if we are talking about a 1KB struct or static array, then the savings may have a significant impact.
Additionally, the extra logic in the move ctor can be elided. DIP 1014 (opPostMove) was born, IIRC, because Weka needs to keep track of the addresses of live instances in some outer code, so saving such re-registering may boost performance as well.
September 05, 2019
On Thursday, 5 September 2019 at 08:29:23 UTC, RazvanN wrote:
> Why not define the move constructor like this:
>
> this(S rhs) {}
>
> I know it takes by value, but up until now it has been referred to as rvalue constructor. Behind the scenes the compiler will treat it as a move constructor and therefore will take the first parameter by ref. Now we have a clear distinction between copy constructor and move constructor. It should probably be in a different overload set than the other constructors.

You and kinke are orbiting around the same idea. Which is essentially making value parameters implicitly rvalue ref. You need to elaborate more on that. How it should behave, and how it would affect existing code.

September 05, 2019
On Thursday, 5 September 2019 at 18:59:50 UTC, kinke wrote:

> Resetting the moved-from instance to T.init or something similar is what you'd do in the move ctor anyway (besides blitting the previous contents into the new instance and maybe doing some more adjustments), and definitely what the default implementation of the move ctor would do.

His initial point about the advantage of rvalue still remains unchallenged.

Example:
```
void foo(@rvalue ref S value, int n = 0)
{
    if (n > 32)
        return;

    foo(__move(value), n + 1);
}

struct S
{
    long[10] a;

    import core.stdc.stdio : puts;

    this(@rvalue ref S) { puts("mc"); }
    this(ref S) { puts("cc"); }
    auto opAssign(@rvalue ref S) { puts("m="); }
    auto opAssign(ref S) { puts("c="); }
    ~this() { puts("~"); }
}

void main()
{
    S lvalue;
    foo(__move(lvalue));
}
```

You can try this with the POC. The whole program only calls the destructor for the lvalue, and only once. You need a competitive alternative.

September 05, 2019
On Thursday, 5 September 2019 at 19:38:57 UTC, Suleyman wrote:
> His initial point about the advantage of rvalue [...]

rvalue ref*
September 05, 2019
On Thursday, 5 September 2019 at 19:38:57 UTC, Suleyman wrote:
> His initial point about the advantage of rvalue still remains unchallenged.
>
> Example:
> ```
> void foo(@rvalue ref S value, int n = 0)
> {
>     if (n > 32)
>         return;
>
>     foo(__move(value), n + 1);
> }
>
> [...]
>
> The whole program only calls the destructor for the lvalue, and only once. You need a competitive alternative.

I know that rvalue refs in the language would enable the same thing, but that's exactly what I'd like to avoid to keep things nice and simple. When using some 3rd-party code, you don't want to depend on them providing the required (and ugly) rvalue ref signatures when coming along with a complex 1KB struct. And there are no rvalue refs in existing code as of now, so existing code bases would have to be uglified to exploit the potential.

This is more or less how I'd imagine it at this time:

struct S
{
    // only called for: `auto s = move(rhs)`
    moveThis(ref S rhs);
    ~this();
    // only called for: `s = move(rhs)`
    opMoveAssign(ref S rhs);
}

// forwarding `auto ref` parameter - unchanged:
void callee(ref S lvalArg);
void callee(S rvalArg);
void foo()(auto ref S s) // either `ref S` reference or `S` value
{
    // ref case: pass along `s` ref
    // value case: `move(s)` (no actual moving, rather imagine forwarding an rvalue ref when coming from C++)
    callee(forward(s));
}

void foo(S param);
// NEW D ABI: non-PODs and large PODs passed by ref, not on the stack.
// Already the case for D on Win64 and how C++ seems to do it generally.
// Also new: `foo` doesn't destruct `param` anymore, that is to be done by the
// caller. That's how C++ does it and currently a hurdle for C++ interop.

void caller(S s)
{
    foo(S());     // construct temporary and pass by ref, then destruct
    foo(s);       // copy-construct temporary and pass by ref, then destruct
    foo(move(s)); // pass `s` directly by ref, then destruct and reset to S.init
}

Potential problems:
* Don't access the moved lvalue anywhere else in the statement.
* An lvalue moved in an argument expression is destructed twice, i.e., 2 times at the same address.
* Cannot represent C++ functions with rvalue refs (except for move ctor and move-assignment operator).
* Some more, I'm sure. ;)
September 05, 2019
On Thu, Sep 5, 2019 at 12:00 PM kinke via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
>
> On Thursday, 5 September 2019 at 18:46:59 UTC, Exil wrote:
> > There's still similar to the problem we have now. You're still doing a memcpy() each time with S.init.
>
> Resetting the moved-from instance to T.init or something similar is what you'd do in the move ctor anyway (besides blitting the previous contents into the new instance and maybe doing some more adjustments), and definitely what the default implementation of the move ctor would do.

I agree the default might do that, but it's equally valid to do
nothing (if destruction has no side-effects), or do swap() if you want
to allow the naturally occurring destructor to tidy up, or various
other strategies.
If we want to allow these potentially efficient implementations, then
perhaps we should mark moved lvalues with a bit saying they have been
invalidated, and the compiler can complain about future references?
September 05, 2019
On Thu, Sep 5, 2019 at 1:30 PM kinke via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
>
> [...]
>
> Potential problems:
> * Don't access the moved lvalue anywhere else in the statement.
> * An lvalue moved in an argument expression is destructed twice,
> i.e., 2 times at the same address.
> * Cannot represent C++ functions with rvalue refs (except for
> move ctor and move-assignment operator).
> * Some more, I'm sure. ;)

We lose by-val calling semantics, which are more efficient for small
struct's (most things), and certain classes of wide-registers in
various architectures (impossible to codify the proper rules in the
language).
I have pondered this same line of thinking to try and preserve
simplicity, but I shot it dead very quickly before I allowed it beyond
my brain ;)

You talk about C++ rval references as if they're complex, but they're
really not. They're easily the best part of C++11.
Their design is a great success, and it is very very well thrught
through. Rejecting ideas from C++ just because they're in C++ is a bad
basis for any decision; C++ has a VERY rigorous process for allowing
language changes. Rval references weren't an initial fault in C++
early design, they were a recent language change, and required a
torturous process to find their way in.
I agree that it is language complexity, and if there's a way to avoid
it, we should absolutely seek it out, but don't flatly disregard the
state of the art. If we can't do BETTER than rval references, then we
shouldn't NOT do rval references just because it's what C++ does (very
successfully).
1 2 3 4 5 6 7 8 9 10 11 12