October 13
On 10/11/2024 8:44 AM, Manu wrote:
> No that's wrong; this is EXACTLY the situation that move semantics exist to address. Move constructor like this should ACTUALLY BE a move constructor!

But currently this(S) is an rvalue constructor, not a move constructor. I've said this many times.

Changing it would break existing code.
October 14
On 10/14/24 04:47, Walter Bright wrote:
> On 10/12/2024 3:12 PM, Timon Gehr wrote:
>> It seems to me that moving a `T` to an `S` and moving a `T` into an rvalue constructor call for an `S` is not really a useful distinction, and the second thing certainly makes sense.
> 
> Moving into an existing object is not the same as moving into an object that has not been constructed yet.

That's the move constructor vs move assign distinction. I was talking about construction as this thread is about move construction.
October 14
On Friday, 11 October 2024 at 16:12:39 UTC, Manu wrote:
> On Thu, 10 Oct 2024, 17:10 Walter Bright via Digitalmars-d, < digitalmars-d@puremagic.com> wrote:
>
>> On 10/8/2024 10:42 PM, Manu wrote:
>> > Can you show us some cases?
>>
>> I'd get infinite recursion with overload resolution, because the compiler
>> will
>> try and match the argument to `S` and `ref S`, made even more complicated
>> with
>> rvalue references enabled.
>>
>
> I don't understand; was the argument an rvalue or an lvalue?
> It is not at all ambiguous or difficult to select the proper overload
> here... one should have been an exact match, the other would have required
> a copy or conversion; making it an obviously less preferable match.
>

```d
struct S
{
    this(ref typeof(this));
    this(typeof(this));
}

void fun(S);

void main()
{
    S a;
    fun(a);
}

```

When the fun(a) is called, the compiler will have to check both
constructors to see which one is a better match. It first tries
the copy constructor and sees that it's an exact match, then it
proceeds to the next overload - the move constructor. Now it wants
to see if the move constructor is callable in this situation.
The move constructor receives its argument by value so the compiler
will think that it needs to call the copy constructor (if it exists). Now, since the copy constructor is in the same overload set as the move constructor, both need to be checked to see their matching levels. => infinite recursion.

That is how overload resolution works for any kind of function (constructor, destructor, normal function). The way to fix this is to either move the copy constructor and the move
constructor into different overload sets or to special case them in the
function resolution algorithm.

RazvanN
October 14
On Friday, 11 October 2024 at 07:25:53 UTC, Manu wrote:
>>
>> Aside from whatever Walter's reasoning is, there are existing constructors in the wild which use
>>
>>     this(S)
>>
>
> I had a moment to think on this case you present.
> It's not clear to me how it actually works now; but I think it would be
> interesting to explore that first; because I think the solution for this
> case being reinterpreted as move-constructor is related (and equally
> solved).
>
> This function receives it's arg BY-VALUE; so it can happily accept an
> r-value... but it's not actually clear how this function receives an lvalue
> even in today's language?
> In order for this function to receive an l-value by-value, it must first
> make a copy to supply as the function's argument (but the function is
> allowed to internally mutate its argument)..
>
> ...but isn't *this* said to be the copy constructor?
>
> This already looks like a chicken/egg problem in today's language... how
> does it create a copy of an lvalue to pass the argument to this function
> without calling this function?
> Whatever answer exists that makes this work equally makes the case work
> where this is reinterpreted as a move-constructor.
>
> The only explanation I can imagine is; because this ISN'T actually a copy
> constructor, the compiler determined that it was appropriate to synthesise
> one (which would be to just copy all members), and so an implicit copy
> constructor DOES actually exist beside this constructor, you just didn't
> write it.
> And assuming that's the case, when this becomes reinterpreted as a move
> constructor, the code remains perfectly correct!
>
The only problem is that in today's language you cannot have a
copy constructor and a move constructor defined. It's an error!
When I implemented the copy constructor I discovered this problem:
if the copy constructor is in the same overload set as the rest of
the constructors then the overload resolution would enter in an infinite
loop, so my proposition was to keep the syntax, but simply place the
copy constructor in a different overload set (__copyctor as opposed to __ctor).
However, Walter was against it and preferred that we disallow the definition
of both copy ctor and rvalue ctor for an object.

However, I think this can be fixed if we keep the same syntax (`this(typeof(this))`) for a move constructor, but simply place it in a different
overload set (__movector). That way, if we have __copyctor, __movector and __ctor the compiler can reason on which one should be called depending on the circumstances and thus infinite loops can be avoided.

> I don't see how elevating this declaration to a move-constructor actually
> changes the semantics at all... I don't think it does? I think this
> function ALREADY IS A MOVE CONSTRUCTOR (at least; it's an
> rvalue-constructor, which is the same thing from a semantic perspective);
> it's just working with an inefficient calling convention.
> Move semantics as proposed simply improve this code, no?


October 14
On Monday, 14 October 2024 at 16:03:15 UTC, RazvanN wrote:

> overload set (__movector). That way, if we have __copyctor, __movector and __ctor the compiler can reason on which one should be called depending on the circumstances and thus infinite loops can be avoided.
>

That doesn't fix the templated copy/move constructor issue, which the
new syntax will probably fix.
October 15
On Mon, 14 Oct 2024 at 12:54, Walter Bright via Digitalmars-d < digitalmars-d@puremagic.com> wrote:

> On 10/11/2024 8:44 AM, Manu wrote:
> > No that's wrong; this is EXACTLY the situation that move semantics exist
> to
> > address. Move constructor like this should ACTUALLY BE a move
> constructor!
>
> But currently this(S) is an rvalue constructor, not a move constructor.
> I've
> said this many times.
>
> Changing it would break existing code.
>

CASE STUDIES

The fact it theoretically exists doesn't make its existence useful or even
sensible.
It *might *break code; but we really need to identify whether that code IS
ALREADY BROKEN. I would give a 75% odds that any code using that pattern is
actually broken already, and I give exactly zero cares if we break already
broken code in service of correcting the single biggest gaping hole at the
heart of D.
I would also put a ~60-80% wager on any real-world instance of that pattern
*already* being some kind of move constructor, which will only benefit from
this change...
I would put maybe 90%+ odds on any given instance of this declaration being
written by a D amateur, who actually didn't understand what they were
writing, and possibly thought it does something that it doesn't actually
do; that is, I would wager with almost 100% probability, that if this
exists in the wild, it's essentially an accident. (probably a failed
attempt at a move constructor)

There is NO CONCEIVABLE USE for this pattern as it is today, outside a
weird syntactic hack with some very weird calling semantics... so; we need
case studies to consider.
We're just going to break this code, and you're just going to have to come
good with that. I will personally submit a PR to every single repo affected
by this change.


October 15
On Mon, 14 Oct 2024 at 12:54, Walter Bright via Digitalmars-d < digitalmars-d@puremagic.com> wrote:

> On 10/11/2024 8:44 AM, Manu wrote:
> > No that's wrong; this is EXACTLY the situation that move semantics exist
> to
> > address. Move constructor like this should ACTUALLY BE a move
> constructor!
>
> But currently this(S) is an rvalue constructor, not a move constructor.
> I've
> said this many times.
>

I've also said many times; an rvalue constructor is a move constructor for
all intents and purposes.
Show me a case study; what might you do with an rvalue constructor if not
initialise an instance from an rvalue?

Changing it would break existing code.
>

That's fine. It's already broken; OR, it's already a move constructor.


October 15
On Tue, 15 Oct 2024 at 01:56, RazvanN via Digitalmars-d < digitalmars-d@puremagic.com> wrote:

> On Friday, 11 October 2024 at 16:12:39 UTC, Manu wrote:
> > On Thu, 10 Oct 2024, 17:10 Walter Bright via Digitalmars-d, < digitalmars-d@puremagic.com> wrote:
> >
> >> On 10/8/2024 10:42 PM, Manu wrote:
> >> > Can you show us some cases?
> >>
> >> I'd get infinite recursion with overload resolution, because
> >> the compiler
> >> will
> >> try and match the argument to `S` and `ref S`, made even more
> >> complicated
> >> with
> >> rvalue references enabled.
> >>
> >
> > I don't understand; was the argument an rvalue or an lvalue?
> > It is not at all ambiguous or difficult to select the proper
> > overload
> > here... one should have been an exact match, the other would
> > have required
> > a copy or conversion; making it an obviously less preferable
> > match.
> >
>
> ```d
> struct S
> {
>      this(ref typeof(this));
>      this(typeof(this));
> }
>
> void fun(S);
>
> void main()
> {
>      S a;
>      fun(a);
> }
>
> ```
>
> When the fun(a) is called, the compiler will have to check both
> constructors to see which one is a better match. It first tries
> the copy constructor and sees that it's an exact match, then it
> proceeds to the next overload - the move constructor. Now it wants
> to see if the move constructor is callable in this situation.
> The move constructor receives its argument by value so the
> compiler
> will think that it needs to call the copy constructor (if it
> exists).


Isn't this the exact moment that the recursion ends? If the copy ctor was an exact match (we must have been supplied an lvalue), and (therefore) while considering the move constructor it was determined that a copy is necessary, then it is not an exact match... copy ctor wins. Case closed.

Now, since the copy constructor is in the same overload
> set as the move constructor, both need to be checked to see their matching levels. => infinite recursion.
>
> That is how overload resolution works for any kind of function
> (constructor, destructor, normal function). The way to fix this
> is to either move the copy constructor and the move
> constructor into different overload sets or to special case them
> in the
> function resolution algorithm.
>

Yeah sorry, I just don't see it.
When the pair are both defined; one is an exact match, and the other is
not. Given an rvalue, the move ctor is an exact match, given an lvalue, the
copy ctor is an exact match. There is no case where this is ambiguous?


October 15
On 15/10/24 11:26, Manu wrote:
> Show me a case study; what might you do with an rvalue constructor if not initialise an instance from an rvalue?

I think one such case might be metaprogramming. Consider:

```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(s1); // This was most certainly not intended as a move constructor.
}
```

This example might seem artificial, and it is, but just imagine any templated constructor that uses DbI, and where the own type matches.

The actual inner details (i.e. that `S` is instantiated) might be even unknown to the caller, for instance if it comes from the user side of an API through a templated function using IFTI.

Also, you cannot use `ref` because you want it to accept both r- and l-values, and in any case there might be good reasons why this isn't desirable in metaprogramming.
October 15
On Tuesday, 15 October 2024 at 09:33:59 UTC, Manu wrote:
> On Tue, 15 Oct 2024 at 01:56, RazvanN via Digitalmars-d < digitalmars-d@puremagic.com> wrote:
>
>> On Friday, 11 October 2024 at 16:12:39 UTC, Manu wrote:
>> > On Thu, 10 Oct 2024, 17:10 Walter Bright via Digitalmars-d, < digitalmars-d@puremagic.com> wrote:
>> >
>> >> On 10/8/2024 10:42 PM, Manu wrote:
>> >> > Can you show us some cases?
>> >>
>> >> I'd get infinite recursion with overload resolution, because
>> >> the compiler
>> >> will
>> >> try and match the argument to `S` and `ref S`, made even more
>> >> complicated
>> >> with
>> >> rvalue references enabled.
>> >>
>> >
>> > I don't understand; was the argument an rvalue or an lvalue?
>> > It is not at all ambiguous or difficult to select the proper
>> > overload
>> > here... one should have been an exact match, the other would
>> > have required
>> > a copy or conversion; making it an obviously less preferable
>> > match.
>> >
>>
>> ```d
>> struct S
>> {
>>      this(ref typeof(this));
>>      this(typeof(this));
>> }
>>
>> void fun(S);
>>
>> void main()
>> {
>>      S a;
>>      fun(a);
>> }
>>
>> ```
>>
>> When the fun(a) is called, the compiler will have to check both
>> constructors to see which one is a better match. It first tries
>> the copy constructor and sees that it's an exact match, then it
>> proceeds to the next overload - the move constructor. Now it wants
>> to see if the move constructor is callable in this situation.
>> The move constructor receives its argument by value so the
>> compiler
>> will think that it needs to call the copy constructor (if it
>> exists).
>
>
> Isn't this the exact moment that the recursion ends? If the copy ctor was an exact match (we must have been supplied an lvalue), and (therefore) while considering the move constructor it was determined that a copy is necessary, then it is not an exact match... copy ctor wins. Case closed.
>

The way overload resolution works is that you try to call match
each function in the overload set and always save (1) the best matching level
up this far, (2) the number of matches and (3)a pointer to the best matching function (and potentially a second pointer to a second function, provided that
you have 2 functions that have the same matching level). Once overload
resolution is done you inspect these results and either pick a single
function or error depending on what you get. This works without any
special casings (there are minor special casings for unique constructors,
but that's fairly non-invasive).

If we were to accept `this(typeof(this))` as a move constructor, we would need
to special case the overload resolution mechanism. I'm not saying it's not possible to implement, rather that we need to add this special case to a
battle tested algorithm.

In contrast, we could leave the overload resolution code untouched and simply
give the move constructor a different identifier (what I mean is, you type `this(S)`, but internally the compiler gives the __movector name to the function). When the compiler inserts calls to the move constructor it will then need to do overload resolution using the name __movector. This seems to me like a more desirable alternative: you get the this(S) syntax (provided that Walter accepts the possibility of code breakage or code fix-up as you call it) and the overload
resolution code remains intact. Additionally, you get smaller overload resolution
penalties given that the overload sets get smaller.

> Now, since the copy constructor is in the same overload
>> set as the move constructor, both need to be checked to see their matching levels. => infinite recursion.
>>
>> That is how overload resolution works for any kind of function
>> (constructor, destructor, normal function). The way to fix this
>> is to either move the copy constructor and the move
>> constructor into different overload sets or to special case them
>> in the
>> function resolution algorithm.
>>
>
> Yeah sorry, I just don't see it.
> When the pair are both defined; one is an exact match, and the other is
> not. Given an rvalue, the move ctor is an exact match, given an lvalue, the
> copy ctor is an exact match. There is no case where this is ambiguous?

Before having copy ctors it was allowed to have  this(ref S) and this(S)
and it worked. So I don't see any opportunity for ambiguity. However, when you
add some implicit constructor calls that's when things get a bit messy, however,
that's not an unsolvable problem. It just depends on what sort of trade-offs you
are willing to make from an implementation stand point.

RazvanN