digitalmars.D - Move Constructors - Converting Lvalues to Rvalues
- Walter Bright (45/45) Sep 30 I've been implementing move constructors. They are working, at least on ...
- Bastiaan Veelo (4/9) Sep 30 I suppose `-preview=rvaluerefparam` is not relevant here, right?
- Guillaume Piolat (3/5) Sep 30 Yes, I kinda expected something like std::move explicit thing.
- Walter Bright (2/3) Sep 30 It should be irrelevant here.
- Manu (4/18) Oct 02 It's essential for this design to work; the logic in that preview allows
- Dukc (2/4) Sep 30 Doesn't `core.lifetime.move` already do this?
- Walter Bright (3/4) Sep 30 Since the compiler doesn't know about it, the potential for optimization...
- Richard (Rikki) Andrew Cattermole (5/10) Sep 30 If only there was a way that was backwards compatible, for us to
- Walter Bright (18/20) Sep 30 It's an interesting idea, but a lot of details would need to be worked o...
- Richard (Rikki) Andrew Cattermole (27/38) Oct 01 Why do you need to?
- Richard (Rikki) Andrew Cattermole (23/23) Oct 01 Now to get annoying again:
- Walter Bright (4/5) Oct 01 A move constructor just has to initialize the object. A move assignment ...
- Manu (5/32) Oct 02 Because D is so fundamentally broken in this department. Work your way
- Dukc (6/10) Oct 01 You can make it to work with a compiler instrinsic. The intrinsic
- Walter Bright (2/6) Oct 01 move() is an unhappy maze of templates. We need something a lot simpler.
- Dukc (4/13) Oct 02 If we'll have a compiler intrinsic function, `move` can be made
- Manu (4/8) Oct 02 That entire file is a malignant cancer and it must be murdered in the fi...
- Timon Gehr (14/17) Sep 30 Well, with DIP1040 this is automatically the case for last uses.
- Walter Bright (8/14) Sep 30 The trouble with last uses is detecting them. It's a variation on live v...
- Per =?UTF-8?B?Tm9yZGzDtnc=?= (5/9) Sep 30 Considering the template-complexity of the current implementation
- Walter Bright (6/8) Sep 30 Manu suggested an intrinsic, something like:
- Per =?UTF-8?B?Tm9yZGzDtnc=?= (2/7) Oct 01 Nice!
- Dukc (4/6) Oct 01 Why special syntax? `move` is a DRuntime function so it's
- Walter Bright (3/5) Oct 01 Converting to an rvalue is an enabler of other functions, too, not just ...
- Timon Gehr (8/14) Oct 01 `move` currently accepts an lvalue and moves it into the return value,
- Walter Bright (2/7) Oct 02 Not exactly. __rvalue would also convert an rvalue to an rvalue.
- Manu (8/16) Oct 02 -preview=rvaluerefparams addresses this; it allows an rvalue to be suppl...
- Timon Gehr (6/26) Oct 02 `-preview=rvaluerefparam` allows you to treat an rvalue as an lvalue
- Manu (12/39) Oct 02 __rvalue() needs to be callable with an lvalue or an rvalue.
- Timon Gehr (6/55) Oct 02 Well, I think you can interpret this as overload resolution between a
- Timon Gehr (42/44) Oct 02 About this, actually it might not do so with `-preview=rvaluerefparam`
- Timon Gehr (3/6) Oct 02 (Sry, Thunderbird did not properly cite this, this was written by Manu,
- Timon Gehr (11/11) Oct 02 One thing that is a bit annoying is:
- Imperatorn (2/9) Oct 01 The __rvalue route makes sense and is intuitive imo.
- Dukc (7/14) Oct 02 `move` _is_ the `__rvalue` building block, at least in principle.
- Timon Gehr (7/17) Oct 01 I think in case we did go the function route, I think any implementation...
- Walter Bright (37/43) Oct 02 The major difference between @move and __rvalue is the former is attache...
- Timon Gehr (57/108) Oct 02 I don't think it affects how overloading works. You just treat it as a
- Imperatorn (2/9) Oct 03 What is the amount we need to pay to not get another attribute?
- ryuukk_ (13/24) Oct 03 Attributes are fine, as long as i don't need to put it on every
- Dukc (10/21) Oct 04 This probably wouldn't be a language attribute like `@nogc` or
- Walter Bright (30/31) Oct 03 What if you have:
- Timon Gehr (9/49) Oct 03 Well, as I said elsewhere, I think the first and third overload are just...
- RazvanN (10/23) Oct 04 You can avoid this by simply moving the move and copy constructor
- Richard (Rikki) Andrew Cattermole (5/28) Oct 04 In this vain, we could use an attribute to differentiate a copy
- Imperatorn (12/19) Oct 02 The intrinsic is clearly best
- Manu (17/70) Oct 02 It's always some weird little detail that changes when I feel like we
- Timon Gehr (16/96) Oct 02 I don't think you are wrong, there are just a few different ways to go
- Walter Bright (24/30) Oct 03 My original plan was to do just that:
- Timon Gehr (7/10) Oct 03 It has to be a move though. An rvalue is owned, an lvalue is not.
- Manu (12/22) Oct 03 Yes, exactly... __rvalue() must be SOME KIND of move; it is explicitly
- Manu (22/61) Oct 03 Okay, fair. I'm convinced.
- Richard (Rikki) Andrew Cattermole (21/21) Sep 30 I suspect that we're going in an entirely wrong direction with move
- Atila Neves (5/10) Sep 30 My understanding is that we need SSA in order to implement
- Richard (Rikki) Andrew Cattermole (7/19) Sep 30 SSA? What? That has nothing to do with it.
- Atila Neves (3/17) Oct 01 Not according to Amaury.
- Richard (Rikki) Andrew Cattermole (17/36) Oct 01 He would be wrong to suggest that it is a requirement. It may make it
- Walter Bright (2/3) Sep 30 Performance.
- Richard (Rikki) Andrew Cattermole (3/7) Sep 30 Ah huh!
- Vindex9 (39/49) Oct 01 ---
- Manu (16/68) Oct 02 I feel like we discussed exactly this at dconf fairly extensively, with
- Jonathan M Davis (10/30) Oct 11 Given the we're now looking at having a separate syntax for move
- Manu (41/70) Oct 11 You really need to re-read the DIP here and understand the design princi...
- Richard (Rikki) Andrew Cattermole (9/17) Oct 11 It may be time for us to get you into a meeting.
- Richard (Rikki) Andrew Cattermole (4/25) Oct 12 I've talked to Mike, he is busy, so I'll be doing the meeting arrangemen...
- Timon Gehr (52/75) Oct 12 I buy this except:
- Timon Gehr (2/6) Oct 12 (Also, it implicitly synthesizes them.)
- Timon Gehr (2/3) Oct 12 (Should say: "convert to `S`" or "convert from `int`".)
I've been implementing move constructors. They are working, at least on my test cases so far. They are distinguished by a copy constructor takes its argument by ref, and a move constructor by value: ``` struct S { this(ref S); // copy constructor this(S); // move constructor } ``` So far, so good. Consider: ``` void phone(S s) { S t = s; // copy constructor } ``` But what if we want to initialize `t` via a move constructor? Somehow, `s` has to be converted from an lvalue to an rvalue. This is done via the function rvalue(): ``` S rvalue(ref S s) { return s; } S t = rvalue(s); ``` Unfortunately, rvalue() gives us this: ``` S __copytmp = 0; this(&__copytmp,&s); // copy construction of s *__H1D1 = __copytmp; return __H1D1; ``` which is correct, but not what we want, which is: ``` return &s; ``` and also not pass an extra hidden parameter for the return value, aka __H1D1. I have thought of several ways to do this, none of which seem particularly attractive as they are all not expressible in conventional D. I won't say what they are in order to not bias anyone. So, does anyone have any brilliant ideas for how to make the compiler treat an lvalue as an rvalue? P.S. C++ uses the std::move function to do it: https://learn.microsoft.com/en-us/cpp/standard-library/utility-functions?view=msvc-170#move which relies on rvalue references: https://learn.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=msvc-170 which is a major feature which I prefer to avoid.
Sep 30
On Monday, 30 September 2024 at 16:05:16 UTC, Walter Bright wrote: [...]P.S. C++ uses the std::move function to do it: https://learn.microsoft.com/en-us/cpp/standard-library/utility-functions?view=msvc-170#move which relies on rvalue references: https://learn.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=msvc-170 which is a major feature which I prefer to avoid.I suppose `-preview=rvaluerefparam` is not relevant here, right? -- Bastiaan.
Sep 30
On Monday, 30 September 2024 at 17:27:25 UTC, Bastiaan Veelo wrote:I suppose `-preview=rvaluerefparam` is not relevant here, right? -- Bastiaan.Yes, I kinda expected something like std::move explicit thing.
Sep 30
On 9/30/2024 10:27 AM, Bastiaan Veelo wrote:I suppose `-preview=rvaluerefparam` is not relevant here, right?It should be irrelevant here.
Sep 30
On Tue, 1 Oct 2024, 03:31 Bastiaan Veelo via Digitalmars-d, < digitalmars-d puremagic.com> wrote:On Monday, 30 September 2024 at 16:05:16 UTC, Walter Bright wrote: [...]It's essential for this design to work; the logic in that preview allows appropriate selection of copy/move overloads.P.S. C++ uses the std::move function to do it:https://learn.microsoft.com/en-us/cpp/standard-library/utility-functions?view=msvc-170#movewhich relies on rvalue references:https://learn.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=msvc-170which is a major feature which I prefer to avoid.I suppose `-preview=rvaluerefparam` is not relevant here, right?
Oct 02
On Monday, 30 September 2024 at 16:05:16 UTC, Walter Bright wrote:So, does anyone have any brilliant ideas for how to make the compiler treat an lvalue as an rvalue?Doesn't `core.lifetime.move` already do this?
Sep 30
On 9/30/2024 10:49 AM, Dukc wrote:Doesn't `core.lifetime.move` already do this?Since the compiler doesn't know about it, the potential for optimizations, lifetime analysis, etc., is lost.
Sep 30
On 01/10/2024 4:22 PM, Walter Bright wrote:On 9/30/2024 10:49 AM, Dukc wrote:If only there was a way that was backwards compatible, for us to annotate on a parameter that it invalidates the input in the form of a move. Perhaps an attribute, like `` move`` in core.attributes would be sufficient for this task ;)Doesn't `core.lifetime.move` already do this?Since the compiler doesn't know about it, the potential for optimizations, lifetime analysis, etc., is lost.
Sep 30
On 9/30/2024 8:29 PM, Richard (Rikki) Andrew Cattermole wrote:Perhaps an attribute, like `` move`` in core.attributes would be sufficient for this task ;)It's an interesting idea, but a lot of details would need to be worked out. For instance, the core.attributes.move is a version of the move assignment operator, not move construction. Then there is the problem of distinguishing between a move constructor and a copy constructor - how do they overload against each other? There's what a default move constructor should be. There's what happens when a field of a struct has a move constructor. And so on. BTW, I took a look at core.lifetime.move. It's hard to figure out just what it does, as there are vacuous forwardings to other templates. For example: ``` void move(T)(ref T source, ref T target) { moveImpl(target, source); } ``` Why?
Sep 30
On 01/10/2024 7:36 PM, Walter Bright wrote:On 9/30/2024 8:29 PM, Richard (Rikki) Andrew Cattermole wrote:Yes!Perhaps an attribute, like `` move`` in core.attributes would be sufficient for this task ;)It's an interesting idea, but a lot of details would need to be worked out. For instance, the core.attributes.move is a version of the move assignment operator, not move construction.Then there is the problem of distinguishing between a move constructor and a copy constructor - how do they overload against each other?Why do you need to? Are they not the same constructor, differing only for the side effects to the caller? It is an assumption that the object you pass in by-ref will be the same object after the function call. ```d int* someObject = ...; int* obj = someObject; callMe(obj); // Does obj == someObject? ``` If you know that ``callMe`` moved the value into it, but never assigned to it, you can elide cleanup on ``obj``. A question I ask myself in any sort of proving topics is what assumptions are being held, and can under normal usage of `` safe`` code can these assumptions be invalidated, potentially desirably. I had a look at C++, it seems the biggest difference between a copy constructor and a move constructor is that a move constructor is mutable by-ref, whereas a copy is read only. Which could indeed be overloaded for in D. And yes, we could infer the difference in this regards in D too. It is what I have been calling "effectively-const". A rather useful thing for owner escape analysis, as it means methods on the owner that do not mutate are callable even if they cannot or have not been annotated ``const``!
Oct 01
Now to get annoying again: What is the difference between move assignment move + reference counting, and move constructors? ```d void caller() { T thing = ...; // rc: 1 call1(thing); call2(thing); // ?reachable thing thing.opRCSub(); // elided due to having been moved thing.__dtor; // elided due to reachable type state } void call1(T thing) { thing.opRCAdd(); // elided due to pair thing.opRCSub(); // elided due to pair thing.__dtor; } void call2( move T thing) { thing.opRCAdd(); // elided due to move thing.opRCSub(); thing.__dtor; } ```
Oct 01
On 10/1/2024 1:59 AM, Richard (Rikki) Andrew Cattermole wrote:Why do you need to?A move constructor just has to initialize the object. A move assignment has to destroy the target first, then move. They are fundamentally different.
Oct 01
On Tue, 1 Oct 2024, 17:03 Walter Bright via Digitalmars-d, < digitalmars-d puremagic.com> wrote:On 9/30/2024 8:29 PM, Richard (Rikki) Andrew Cattermole wrote:Because D is so fundamentally broken in this department. Work your way through a clear understanding of core.lifetime, and you should become convinced how important this work you are doing actually it's!Perhaps an attribute, like `` move`` in core.attributes would besufficient forthis task ;)It's an interesting idea, but a lot of details would need to be worked out. For instance, the core.attributes.move is a version of the move assignment operator, not move construction. Then there is the problem of distinguishing between a move constructor and a copy constructor - how do they overload against each other? There's what a default move constructor should be. There's what happens when a field of a struct has a move constructor. And so on. BTW, I took a look at core.lifetime.move. It's hard to figure out just what it does, as there are vacuous forwardings to other templates. For example: ``` void move(T)(ref T source, ref T target) { moveImpl(target, source); } ``` Why?
Oct 02
On Tuesday, 1 October 2024 at 03:22:33 UTC, Walter Bright wrote:On 9/30/2024 10:49 AM, Dukc wrote:You can make it to work with a compiler instrinsic. The intrinsic would either be an UDA applied to `move` that allows the compiler to make those assumptions, or an intrinsic function that works along the lines of `nove` and `moveEmplace`. The public DRuntime functions would forward or alias to the instrinsic function.Doesn't `core.lifetime.move` already do this?Since the compiler doesn't know about it, the potential for optimizations, lifetime analysis, etc., is lost.
Oct 01
On 10/1/2024 12:25 AM, Dukc wrote:You can make it to work with a compiler instrinsic. The intrinsic would either be an UDA applied to `move` that allows the compiler to make those assumptions, or an intrinsic function that works along the lines of `nove` and `moveEmplace`. The public DRuntime functions would forward or alias to the instrinsic function.move() is an unhappy maze of templates. We need something a lot simpler.
Oct 01
On Tuesday, 1 October 2024 at 17:41:34 UTC, Walter Bright wrote:On 10/1/2024 12:25 AM, Dukc wrote:If we'll have a compiler intrinsic function, `move` can be made much simpler by having it simply call the compiler intrinsic, or alias to it.You can make it to work with a compiler instrinsic. The intrinsic would either be an UDA applied to `move` that allows the compiler to make those assumptions, or an intrinsic function that works along the lines of `nove` and `moveEmplace`. The public DRuntime functions would forward or alias to the instrinsic function.move() is an unhappy maze of templates. We need something a lot simpler.
Oct 02
On Tue, 1 Oct 2024, 03:51 Dukc via Digitalmars-d, < digitalmars-d puremagic.com> wrote:On Monday, 30 September 2024 at 16:05:16 UTC, Walter Bright wrote:That entire file is a malignant cancer and it must be murdered in the fires of hell.So, does anyone have any brilliant ideas for how to make the compiler treat an lvalue as an rvalue?Doesn't `core.lifetime.move` already do this?
Oct 02
On 9/30/24 18:05, Walter Bright wrote:So, does anyone have any brilliant ideas for how to make the compiler treat an lvalue as an rvalue?Well, with DIP1040 this is automatically the case for last uses. So in your code example: ``` void phone(S s) { S t = s; // copy constructor } ``` The comment is actually inaccurate with DIP1040. With DIP1040, this is a move. Otherwise, maybe expose explicit moves. This is useful generally. Can just be `move(x)`, where `move` is either special, or is a function with a "move by default" attribute on its parameter, as we discussed previously.
Sep 30
On 9/30/2024 12:37 PM, Timon Gehr wrote:On 9/30/24 18:05, Walter Bright wrote:The trouble with last uses is detecting them. It's a variation on live variable DFA, which the optimizer does for variables. Indirections cannot be reliably tracked, and variables which have their address taken similarly can't reliably have DFA done on them. In any case, I decided for at least the first implementation to require an explicit "convert to rvalue" operation to get the benefits of a move construction. I'm pretty sure it will deliver most of what we want.So, does anyone have any brilliant ideas for how to make the compiler treat an lvalue as an rvalue?Well, with DIP1040 this is automatically the case for last uses.
Sep 30
On Monday, 30 September 2024 at 19:37:04 UTC, Timon Gehr wrote:Otherwise, maybe expose explicit moves. This is useful generally. Can just be `move(x)`, where `move` is either special, or is a function with a "move by default" attribute on its parameter, as we discussed previously.Considering the template-complexity of the current implementation of `core.lifetime.move` it will beneficial compile-time wise to make `move` become a builtin. Ideally I would like to have a special syntax for converting an l-value to an r-value.
Sep 30
On 9/30/2024 9:55 PM, Per Nordlöw wrote:Ideally I would like to have a special syntax for converting an l-value to an r-value.Manu suggested an intrinsic, something like: ``` __rvalue(s) ``` where __rvalue is a keyword.
Sep 30
On Tuesday, 1 October 2024 at 06:39:21 UTC, Walter Bright wrote:Manu suggested an intrinsic, something like: ``` __rvalue(s) ``` where __rvalue is a keyword.Nice!
Oct 01
On Tuesday, 1 October 2024 at 04:55:43 UTC, Per Nordlöw wrote:Ideally I would like to have a special syntax for converting an l-value to an r-value.Why special syntax? `move` is a DRuntime function so it's expected that it can have special semantics, even without special syntax.
Oct 01
On 10/1/2024 12:27 AM, Dukc wrote:Why special syntax? `move` is a DRuntime function so it's expected that it can have special semantics, even without special syntax.Converting to an rvalue is an enabler of other functions, too, not just move. __rvalue is a building block, not a complete function.
Oct 01
On 10/1/24 19:37, Walter Bright wrote:On 10/1/2024 12:27 AM, Dukc wrote:`move` currently accepts an lvalue and moves it into the return value, leaving `.init` in the argument. I guess the new implementation you have in mind is something like the following? ```d auto move(T)(ref T arg)=>__rvalue(arg); ```Why special syntax? `move` is a DRuntime function so it's expected that it can have special semantics, even without special syntax.Converting to an rvalue is an enabler of other functions, too, not just move. __rvalue is a building block, not a complete function.
Oct 01
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.
Oct 02
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:-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.I guess the new implementation you have in mind is something like thefollowing?```d auto move(T)(ref T arg)=>__rvalue(arg); ```Not exactly. __rvalue would also convert an rvalue to an rvalue.
Oct 02
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.
Oct 02
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:__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.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.
Oct 02
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.
Oct 02
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.
Oct 02
On 10/3/24 02:57, Timon Gehr wrote:On 10/3/24 02:42, Timon Gehr wrote:(Sry, Thunderbird did not properly cite this, this was written by Manu, not me.)Later if you add a move constructor, it will automatically select that as the appropriate choice.
Oct 02
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...
Oct 02
On Tuesday, 1 October 2024 at 17:37:37 UTC, Walter Bright wrote:On 10/1/2024 12:27 AM, Dukc wrote:The __rvalue route makes sense and is intuitive imo.Why special syntax? `move` is a DRuntime function so it's expected that it can have special semantics, even without special syntax.Converting to an rvalue is an enabler of other functions, too, not just move. __rvalue is a building block, not a complete function.
Oct 01
On Tuesday, 1 October 2024 at 17:37:37 UTC, Walter Bright wrote:On 10/1/2024 12:27 AM, Dukc wrote:`move` _is_ the `__rvalue` building block, at least in principle. It accepts an lvalue and returns a rvalue, without executing the destructor or move constructor of the type in question. It does set the lvalue to it's `.init` value, but this can be elided by the optimiser if the lvalue is not used again, assuming the destructor doesn't side-effect in any way.Why special syntax? `move` is a DRuntime function so it's expected that it can have special semantics, even without special syntax.Converting to an rvalue is an enabler of other functions, too, not just move. __rvalue is a building block, not a complete function.
Oct 02
On 10/1/24 06:55, Per Nordlöw wrote:On Monday, 30 September 2024 at 19:37:04 UTC, 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; ``` Of course, going with a built-in makes sense too.Otherwise, maybe expose explicit moves. This is useful generally. Can just be `move(x)`, where `move` is either special, or is a function with a "move by default" attribute on its parameter, as we discussed previously.Considering the template-complexity of the current implementation of `core.lifetime.move` it will beneficial compile-time wise to make `move` become a builtin. Ideally I would like to have a special syntax for converting an l-value to an r-value.
Oct 01
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.
Oct 02
On 10/2/24 20:00, Walter Bright wrote:On 10/1/2024 1:06 PM, Timon Gehr wrote: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.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. ...`__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.
Oct 02
On Wednesday, 2 October 2024 at 18:57:24 UTC, Timon Gehr wrote:On 10/2/24 20:00, Walter Bright wrote:What is the amount we need to pay to not get another attribute?[...]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. [...]
Oct 03
On Thursday, 3 October 2024 at 14:27:37 UTC, Imperatorn wrote:On Wednesday, 2 October 2024 at 18:57:24 UTC, Timon Gehr wrote:Attributes are fine, as long as i don't need to put it on every functions because this random one has an attribute I actually would like more attributes, for builtin functionalities, like cDefine on imports to avoid having to pollute my makefile with duplicates Move constructor in interesting tho, it only appeal to C++ developers, a dangerous species that leads to a language nobody wants to use Look at these C like languages gaining momentum, they embrace features that doesn't require one to fall into RAII/OOP traps, D will accumulate yet another feature that only appeal to C++ crowd that lead to stupid APIs, yet, even Andrei went back to C++On 10/2/24 20:00, Walter Bright wrote:What is the amount we need to pay to not get another attribute?[...]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. [...]
Oct 03
On Thursday, 3 October 2024 at 14:27:37 UTC, Imperatorn wrote:On Wednesday, 2 October 2024 at 18:57:24 UTC, Timon Gehr wrote:This probably wouldn't be a language attribute like ` nogc` or ` trusted`. It would be a simple UDA that is defined as a compiler intrinsic. It would likely be defined either at [core.attribute](https://dlang.org/phobos/core_attribute.html), or somewhere at `core.internal`. And in any case it wouldn't be anything the D programmer has to deal with. It'd be used to define DRuntime functions, and maybe some low-level Phobos / utility library functions. But not at application code, nor even at normal library code.On 10/2/24 20:00, Walter Bright wrote:What is the amount we need to pay to not get another attribute?[...]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. [...]
Oct 04
What if you have: ``` void foo(S); void foo(ref S); void foo( move S); ``` ? The overloading rules are already much more complex than I intended. ``` S foo(S s)=>s; ```There is actually no reason for this to make a copy, and the compiler wouldin principle be able to figure it out. I did consider it (actually `S foo(ref S s)=>s;`), but didn't like it because it would require major changes in multiple places in some already complex code. It was too high risk of introducing all kinds of unexpected interactions and problems. And, in the end, __lvalue(s) does the same thing and is fairly simple. I'm also currently struggling with: ``` this(S); this(ref S); ``` and then trying to get "is it copyable" to work. The trouble is that a move constructor not only has an S for an rvalue, that S must also be the same as the struct name. This cannot be determined by the parser, it needs semantic analysis. This means that when the constructor is searched for, it has to exclude matching on a move constructor, that cannot be detected before semantic analysis. The solution will likely be adding another parameter to the name lookup. Sigh, more slowdowns and complexity. The __rvalue thing, on the other hand, requires (I think) very little disruption to the compiler and language.
Oct 03
On 10/3/24 17:35, Walter Bright wrote:What if you have: ``` void foo(S); void foo(ref S); void foo( move S); ``` ? The overloading rules are already much more complex than I intended. ...Well, as I said elsewhere, I think the first and third overload are just ambiguous with each other. They both prefer rvalues, of the same type.``` S foo(S s)=>s; ``` > There is actually no reason for this to make a copy, and the compiler would in principle be able to figure it out. I did consider it (actually `S foo(ref S s)=>s;`),Well, with `ref` it does not work because ownership remains with the caller.but didn't like it because it would require major changes in multiple places in some already complex code. It was too high risk of introducing all kinds of unexpected interactions and problems. And, in the end, __lvalue(s) does the same thing and is fairly simple. I'm also currently struggling with: ``` this(S); this(ref S); ``` and then trying to get "is it copyable" to work. The trouble is that a move constructor not only has an S for an rvalue, that S must also be the same as the struct name. This cannot be determined by the parser, it needs semantic analysis. This means that when the constructor is searched for, it has to exclude matching on a move constructor, that cannot be detected before semantic analysis. ...Sounds annoying, but I guess this is a general issue, as metaprogramming can be involved in the declaration of constructors.The solution will likely be adding another parameter to the name lookup. Sigh, more slowdowns and complexity. The __rvalue thing, on the other hand, requires (I think) very little disruption to the compiler and language.Well, `__rvalue` is fine. The question is just is it really sensible to tie move semantics to pass by value, and likely the answer is "not really". It is already obvious with the move constructor design.
Oct 03
On Thursday, 3 October 2024 at 15:35:36 UTC, Walter Bright wrote:What if you have:I'm also currently struggling with: ``` this(S); this(ref S); ``` and then trying to get "is it copyable" to work. The trouble is that a move constructor not only has an S for an rvalue, that S must also be the same as the struct name. This cannot be determined by the parser, it needs semantic analysis. This means that when the constructor is searched for, it has to exclude matching on a move constructor, that cannot be detected before semantic analysis.You can avoid this by simply moving the move and copy constructor to different overload sets during semantic analysis (__ctor, __cpctor, __mvctor). This will simplify the code immensely and it also has the added benefit that move and copy constructor will not be considered for manual constructor calls, which makes sense because mv and cp ctor are not meant for that.
Oct 04
On 04/10/2024 8:15 PM, RazvanN wrote:On Thursday, 3 October 2024 at 15:35:36 UTC, Walter Bright wrote:In this vain, we could use an attribute to differentiate a copy constructor and move constructor. Same signature apart from the attribute. There is no reason why a move constructor couldn't do some cleanup of the old value, to make it safe for a destructor call on it.What if you have:I'm also currently struggling with: ``` this(S); this(ref S); ``` and then trying to get "is it copyable" to work. The trouble is that a move constructor not only has an S for an rvalue, that S must also be the same as the struct name. This cannot be determined by the parser, it needs semantic analysis. This means that when the constructor is searched for, it has to exclude matching on a move constructor, that cannot be detected before semantic analysis.You can avoid this by simply moving the move and copy constructor to different overload sets during semantic analysis (__ctor, __cpctor, __mvctor). This will simplify the code immensely and it also has the added benefit that move and copy constructor will not be considered for manual constructor calls, which makes sense because mv and cp ctor are not meant for that.
Oct 04
On Wednesday, 2 October 2024 at 18:00:29 UTC, Walter Bright wrote:On 10/1/2024 1:06 PM, Timon Gehr wrote:The intrinsic is clearly best ```d struct S { this(ref S) { /* Copy */ } this(S) { /* Move */ } } void main() { S t; S s1 = t; // Copy S s2 = __rvalue(t); // Move ```[...]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. [...]
Oct 02
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: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 think in case we did go the function route, I think any implementationof`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.
Oct 02
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.
Oct 02
On 10/2/2024 4:17 PM, Manu wrote: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?My original plan was to do just that: ``` S move(ref S s) => s; ``` would be recognized and replaced with the `__lvalue(s)`. What bothered me about it was the magic behavior of it, such as what happens with: ``` S move(ref S s, int i) => s; ``` which will behave completely differently, and may be quite surprising to users. I expect somebody will inevitably set at store on: ``` S move(ref S s) => s; S move(ref S s, int i) => s; ``` doing the same thing, but they don't, and the user will be baffled and I will get bug reports and the language will become more complicated is in the ugly way C++ became.'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...__rvalue() does nothing more than say its argument is not an lvalue, so it will not match with a `ref` parameter. Lifetime analysis occurs after overload resolution, so I don't think it will impact it. __rvalue() is not a move, it just guides the overload resolution to the move constructor/assignment. It's a hint.
Oct 03
On 10/3/24 17:47, Walter Bright wrote:__rvalue() is not a move, it just guides the overload resolution to the move constructor/assignment. It's a hint.It has to be a move though. An rvalue is owned, an lvalue is not. Whenever an lvalue goes into an rvalue, it is either copy or move. If `__rvalue` is just some sort of ad-hoc type-punning operation exclusively for overload resolution, it will often copy the argument, unless your intention is to force it to be consumed by a function that is implicitly ` move` (such as a move constructor or a move assignment).
Oct 03
On Fri, 4 Oct 2024 at 06:56, Timon Gehr via Digitalmars-d < digitalmars-d puremagic.com> wrote:On 10/3/24 17:47, Walter Bright wrote:Yes, exactly... __rvalue() must be SOME KIND of move; it is explicitly taking ownership away from the owner, and handing it to some new owner. The thing about __rvalue() is that you will hide it inside some function, like `T move(ref T t) => __rvalue(t)` That encapsulates the ownership transfer, and separates the visibility of the ownership transfer from the calling scope where that information is needed. Maybe you should reserve the name `move` and make it an error for a user to produce any declaration with that name? That might avoid your surprise overload issue?__rvalue() is not a move, it just guides the overload resolution to the move constructor/assignment. It's a hint.It has to be a move though. An rvalue is owned, an lvalue is not. Whenever an lvalue goes into an rvalue, it is either copy or move. If `__rvalue` is just some sort of ad-hoc type-punning operation exclusively for overload resolution, it will often copy the argument, unless your intention is to force it to be consumed by a function that is implicitly ` move` (such as a move constructor or a move assignment).
Oct 03
On Fri, 4 Oct 2024 at 01:51, Walter Bright via Digitalmars-d < digitalmars-d puremagic.com> wrote:On 10/2/2024 4:17 PM, Manu wrote:Okay, fair. I'm convinced.your __rvalue() is essentially the `T move(ref T)` intrinsic wediscussed atlength; so why not just make the move intrinsic rather than this thing?My original plan was to do just that: ``` S move(ref S s) => s; ``` would be recognized and replaced with the `__lvalue(s)`. What bothered me about it was the magic behavior of it, such as what happens with: ``` S move(ref S s, int i) => s; ``` which will behave completely differently, and may be quite surprising to users. I expect somebody will inevitably set at store on: ``` S move(ref S s) => s; S move(ref S s, int i) => s; ``` doing the same thing, but they don't, and the user will be baffled and I will get bug reports and the language will become more complicated is in the ugly way C++ became.'move' is the operation we seek, and it hasOkay, so then from this perspective without any obvious place to pin any lifetime tracking semantics, the lifetime of an argument to move will continue after the call to move; which implies (as we discussed at some length) that the callee MAY move the object, and if it does, the callee is responsible for putting it back in an init state so that it is valid when the calling object is destructed at some later time... This is fine, and we tolerate this arrangement in C++ (probably for the exact same reasons), but it's not quite an optimal solution. Sufficiently capable lifetime tracking could theoretically elide calls to destructors when it knows a move took place, but I can't see a way the calling scope can have any such confidence with __rvalue() as a tool? I guess this is where Timon's proposal for move comes in. Incidentally, that proposal seems to be additive to this work though; it could be added later to give the calling scope that information, but it doesn't seem to contradict your current path. I think I'm satisfied with your direction. It'd be interesting if you could push a working branch? I've been writing a whole bunch of containers the last few days and I'd like to try it out if it's in a testable state.lifetime related semantics involved with the operation. __rvalue()doesn't feellike the right abstraction for that set of semantics; you're gonna findyourselfwondering where to pin the lifetime semantics in the future...__rvalue() does nothing more than say its argument is not an lvalue, so it will not match with a `ref` parameter. Lifetime analysis occurs after overload resolution, so I don't think it will impact it. __rvalue() is not a move, it just guides the overload resolution to the move constructor/assignment. It's a hint.
Oct 03
I suspect that we're going in an entirely wrong direction with move constructors for two reasons: 1. Nobody has been able to answer what the purpose of them is, is it an optimization, is it ownership transfer system (which is better done with isolated). 2. move on the parameter. This can be inferred, AND applies to other functions like swap for escape analysis and opAssign! ```d struct S { this( move ref S s) { // copy constructor with the /side effect/ of invaliding the original, call dependent s = S.init; // compiler removes this if seen } } void phone(S s1) { S s2 = s1; // only one of these has to invalidate s1 S s3 = s1; } ``` I strongly believe not going the attribute route is blocking us in for worse behaviors.
Sep 30
On Monday, 30 September 2024 at 20:28:05 UTC, Richard (Rikki) Andrew Cattermole wrote:I suspect that we're going in an entirely wrong direction with move constructors for two reasons: 1. Nobody has been able to answer what the purpose of them is, is it an optimization, is it ownership transfer system (which is better done with isolated).My understanding is that we need SSA in order to implement isolated. And so for now I'd say it's an optimisation mostly, but also bug prevention with move-only types.
Sep 30
On 01/10/2024 3:58 PM, Atila Neves wrote:On Monday, 30 September 2024 at 20:28:05 UTC, Richard (Rikki) Andrew Cattermole wrote:SSA? What? That has nothing to do with it. If you want isolated to not have its own analysis you need type state analysis, which is DFA. The same DFA we should be using for escape analysis. Forward pass only on success. One of my concerns for move constructors is the addition of analysis that is in addition to the escape analysis DFA.I suspect that we're going in an entirely wrong direction with move constructors for two reasons: 1. Nobody has been able to answer what the purpose of them is, is it an optimization, is it ownership transfer system (which is better done with isolated).My understanding is that we need SSA in order to implement isolated. And so for now I'd say it's an optimisation mostly, but also bug prevention with move-only types.
Sep 30
On Tuesday, 1 October 2024 at 03:07:02 UTC, Richard (Rikki) Andrew Cattermole wrote:On 01/10/2024 3:58 PM, Atila Neves wrote:Not according to Amaury.On Monday, 30 September 2024 at 20:28:05 UTC, Richard (Rikki) Andrew Cattermole wrote:SSA? What? That has nothing to do with it.I suspect that we're going in an entirely wrong direction with move constructors for two reasons: 1. Nobody has been able to answer what the purpose of them is, is it an optimization, is it ownership transfer system (which is better done with isolated).My understanding is that we need SSA in order to implement isolated. And so for now I'd say it's an optimisation mostly, but also bug prevention with move-only types.
Oct 01
On 02/10/2024 3:59 PM, Atila Neves wrote:On Tuesday, 1 October 2024 at 03:07:02 UTC, Richard (Rikki) Andrew Cattermole wrote:He would be wrong to suggest that it is a requirement. It may make it easier to conceptualize it, and if you already have an IR that is SSA it may even allow you to do it cheaply. But it is not a requirement. You can confirm what I have said by reading Wikipedia on the subject where SSA is indeed listed as an alternative approach to some problems. https://en.wikipedia.org/wiki/Data-flow_analysis To construct an SSA IR will cost a lot of time, that quite frankly I can't see making the rest of the analysis faster. It would be a totally different situation if we could parallelize it (I went down that path with my semantic 4, but it makes more sense to do as part of semantic 3 for this). You basically need to make the CFG from the AST. Isolated and with that unique ownership is based upon the data flow analysis of the tracked objects. Object tracking isn't too complex if you are limited to forward pass only. For reference, good article by one of the creators of Midori, regarding isolated https://joeduffyblog.com/2016/11/30/15-years-of-concurrency/On 01/10/2024 3:58 PM, Atila Neves wrote:Not according to Amaury.On Monday, 30 September 2024 at 20:28:05 UTC, Richard (Rikki) Andrew Cattermole wrote:SSA? What? That has nothing to do with it.I suspect that we're going in an entirely wrong direction with move constructors for two reasons: 1. Nobody has been able to answer what the purpose of them is, is it an optimization, is it ownership transfer system (which is better done with isolated).My understanding is that we need SSA in order to implement isolated. And so for now I'd say it's an optimisation mostly, but also bug prevention with move-only types.
Oct 01
On 9/30/2024 1:28 PM, Richard (Rikki) Andrew Cattermole wrote:1. Nobody has been able to answer what the purpose of them isPerformance.
Sep 30
On 01/10/2024 4:11 PM, Walter Bright wrote:On 9/30/2024 1:28 PM, Richard (Rikki) Andrew Cattermole wrote:Ah huh! Now I'm on board with us having move constructors.1. Nobody has been able to answer what the purpose of them isPerformance.
Sep 30
On Monday, 30 September 2024 at 16:05:16 UTC, Walter Bright wrote:I've been implementing move constructors. They are working, at least on my test cases so far. They are distinguished by a copy constructor takes its argument by ref, and a move constructor by value: ``` struct S { this(ref S); // copy constructor this(S); // move constructor }--- Is it necessary to breed entities? Doesn't std.algorithm.move do everything we need? ```d import core.stdc.stdlib : malloc, free; import core.stdc.string : memcpy; import std.algorithm.mutation : move; import std.exception : enforce; import std.stdio : writeln; struct S { void* content; size_t size; this(size_t n) { writeln("regular ctor"); // debug init(n); } this(ref return scope const S rhs) { writeln("copy ctor"); // debug init(rhs.size); memcpy(this.content, rhs.content, rhs.size); } private void init(size_t n) { this.content = malloc(n); enforce(this.content != null, "Memory allocation error."); this.size = n; } } void main() { S obj1 = S(1024); assert(obj1.content != null); S obj2 = move(obj1); // moving assert(obj1.content == null); assert(obj2.content != null); S obj3 = S(obj2); // copying assert(obj3.content != obj2.content); assert(obj3.size == obj2.size); } ```
Oct 01
I feel like we discussed exactly this at dconf fairly extensively, with pictures and diagrams and stuff... Just write `T move(ref T)` as an intrinsic. There's potentially more work to do than this operation; the intrinsic will also (in the future) include internal logic to end the lifetime of the memory region, and possibly logic to record the transfer of ownership for the value. This is all not expressible in-language... it's most appropriate as an intrinsic; this is mega-fundamental stuff. Remember, the goal is to murder core.lifetime with fire. We don't want a function for this, no call/ret, do not want to pollute the program flow in that way and definitely don't want to create weird program flow while stepping through code while debugging. Intrinsic that initially just strips off ref, and can be enhanced with lifetime management logic in future. On Tue, 1 Oct 2024, 02:11 Walter Bright via Digitalmars-d, < digitalmars-d puremagic.com> wrote:I've been implementing move constructors. They are working, at least on my test cases so far. They are distinguished by a copy constructor takes its argument by ref, and a move constructor by value: ``` struct S { this(ref S); // copy constructor this(S); // move constructor } ``` So far, so good. Consider: ``` void phone(S s) { S t = s; // copy constructor } ``` But what if we want to initialize `t` via a move constructor? Somehow, `s` has to be converted from an lvalue to an rvalue. This is done via the function rvalue(): ``` S rvalue(ref S s) { return s; } S t = rvalue(s); ``` Unfortunately, rvalue() gives us this: ``` S __copytmp = 0; this(&__copytmp,&s); // copy construction of s *__H1D1 = __copytmp; return __H1D1; ``` which is correct, but not what we want, which is: ``` return &s; ``` and also not pass an extra hidden parameter for the return value, aka __H1D1. I have thought of several ways to do this, none of which seem particularly attractive as they are all not expressible in conventional D. I won't say what they are in order to not bias anyone. So, does anyone have any brilliant ideas for how to make the compiler treat an lvalue as an rvalue? P.S. C++ uses the std::move function to do it: https://learn.microsoft.com/en-us/cpp/standard-library/utility-functions?view=msvc-170#move which relies on rvalue references: https://learn.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=msvc-170 which is a major feature which I prefer to avoid.
Oct 02
On Monday, September 30, 2024 10:05:16 AM MDT Walter Bright via Digitalmars-d wrote:I've been implementing move constructors. They are working, at least on my test cases so far. They are distinguished by a copy constructor takes its argument by ref, and a move constructor by value: ``` struct S { this(ref S); // copy constructor this(S); // move constructor } ``` So far, so good. Consider: ``` void phone(S s) { S t = s; // copy constructor } ``` But what if we want to initialize `t` via a move constructor? Somehow, `s` has to be converted from an lvalue to an rvalue. This is done via the function rvalue(): ```Given the we're now looking at having a separate syntax for move constructors, I would argue that they should just have to be ref, which should eliminate the special cases that you're fighting here. Even if an rvalue is being passed in, it has to be _somewhere_ as a temporary, and fundamentally, a move constructor needs an lvalue, since it's moving an object from one location to another, even if the source location is a temporary one. - Jonathan M Davis
Oct 11
On Sat, 12 Oct 2024 at 04:20, Jonathan M Davis via Digitalmars-d < digitalmars-d puremagic.com> wrote:On Monday, September 30, 2024 10:05:16 AM MDT Walter Bright via Digitalmars-d wrote:You really need to re-read the DIP here and understand the design principle of this whole thing. This suggestion show us that you either don't understand the DIP, totally missed the point, or possibly that you fundamentally disagree with the DIP; and if that's the case, I think you need to present that argument and what you'd propose instead, rather than talk with respect to implementation of the DIP with a completely different idea in mind. If you change the fundamental substance of the DIP, it's a new DIP. The proposal in the DIP is very simple; struct rvalues are ref too now, so don't worry about the ref stuff; everything's a ref. The problem to be solved is, how do we appropriately distinguish an rvalue from an lvalue; and while we've had a myriad of proposals adding attributes, Walter found an arrangement where the distinction can be expressed in existing language in an astonishingly elegant way; recognise that by-value calls (accepts rvalue) are all actually move opportunities. void f(ref T) // arg is an lvalue; the syntax says "I have received a reference to *someone else's* thing"; or put another way, the callee does NOT own the argument. void f(T) // arg is an rvalue; this syntax says "I have received this thing"; the callee owns the argument, and as such, is a valid recipient of any move operation. In order to make move operations actually move operations, they need to be passed by ref (by *rvalue-ref*, so to speak), and that is literally the entire point of the DIP; the calling convention is adjusted so a by-value (r-value) parameter is passed by rvalue-ref. This is what I mean where I say we're talking about "move semantics", but everyone seems to be fixated on move constructors as if they're an extraordinarily interesting part of this story. A move constructor is just an efficient initialisation opportunity, and it's a small part of the overall story regarding move semantics. As I see it; the move constructor *must *be an overload; otherwise, surely you must assume the overload selection rules don't work, and so every single other function (aside from the move constructor which received a blessed hack!) will not make a proper selection. Move semantics ARE proper overload selection, that's the entire meat of this design. This design *IS* overload selection rules.... to try and work that problem off to the side would be to have completely missed the point. If you exclude overload resolution from the picture, then I don't even know what we're talking about here.I've been implementing move constructors. They are working, at least onmytest cases so far. They are distinguished by a copy constructor takes its argument by ref, and a move constructor by value: ``` struct S { this(ref S); // copy constructor this(S); // move constructor } ``` So far, so good. Consider: ``` void phone(S s) { S t = s; // copy constructor } ``` But what if we want to initialize `t` via a move constructor? Somehow,`s`has to be converted from an lvalue to an rvalue. This is done via the function rvalue(): ```Given the we're now looking at having a separate syntax for move constructors, I would argue that they should just have to be ref, which should eliminate the special cases that you're fighting here.
Oct 11
On 12/10/2024 6:56 PM, Manu wrote:You really need to re-read the DIP here and understand the design principle of this whole thing. This suggestion show us that you either don't understand the DIP, totally missed the point, or possibly that you fundamentally disagree with the DIP; and if that's the case, I think you need to present that argument and what you'd propose instead, rather than talk with respect to implementation of the DIP with a completely different idea in mind. If you change the fundamental substance of the DIP, it's a new DIP.It may be time for us to get you into a meeting. Yesterday in the monthly meeting it was determined that Martin and you should talk about moving. He and I have some similar ideas regarding things like postblit and their interaction with it. So I am confident in a good outcome if he is leading it. I'll message Mike to reaffirm what I've said here and see if he can't arrange something between everyone.
Oct 11
On 12/10/2024 7:42 PM, Richard (Rikki) Andrew Cattermole wrote:On 12/10/2024 6:56 PM, Manu wrote:I've talked to Mike, he is busy, so I'll be doing the meeting arrangement. If you or anyone else who wants to be there but isn't on the email chain please let me know.You really need to re-read the DIP here and understand the design principle of this whole thing. This suggestion show us that you either don't understand the DIP, totally missed the point, or possibly that you fundamentally disagree with the DIP; and if that's the case, I think you need to present that argument and what you'd propose instead, rather than talk with respect to implementation of the DIP with a completely different idea in mind. If you change the fundamental substance of the DIP, it's a new DIP.It may be time for us to get you into a meeting. Yesterday in the monthly meeting it was determined that Martin and you should talk about moving. He and I have some similar ideas regarding things like postblit and their interaction with it. So I am confident in a good outcome if he is leading it. I'll message Mike to reaffirm what I've said here and see if he can't arrange something between everyone.
Oct 12
On 10/12/24 07:56, Manu wrote:The proposal in the DIP is very simple; struct rvalues are ref too now, so don't worry about the ref stuff; everything's a ref. The problem to be solved is, how do we appropriately distinguish an rvalue from an lvalue; and while we've had a myriad of proposals adding attributes, Walter found an arrangement where the distinction can be expressed in existing language in an astonishingly elegant way; recognise that by- value calls (accepts rvalue) are all actually move opportunities. void f(ref T) // arg is an lvalue; the syntax says "I have received a reference to /someone else's/ thing"; or put another way, the callee does NOT own the argument. void f(T) // arg is an rvalue; this syntax says "I have received this thing"; the callee owns the argument, and as such, is a valid recipient of any move operation. In order to make move operations actually move operations, they need to be passed by ref (by /rvalue-ref/, so to speak), and that is literally the entire point of the DIP; the calling convention is adjusted so a by- value (r-value) parameter is passed by rvalue-ref. This is what I mean where I say we're talking about "move semantics", but everyone seems to be fixated on move constructors as if they're an extraordinarily interesting part of this story.I buy this except: - With DIP1040, a move constructor `this(S)` is actually fundamentally different from a function `f(S)`, and not just because it is a constructor. I think this should be adjusted so it works the way you say. Then there can be some special syntax to elide destructor calls explicitly. - Move and copy constructors are indeed a bit special because the compiler may implicitly call them in ways it would not call any other constructor. Here, idk. It's not really an important concern to me personally but I can see why someone might have an issue with this. For copy constructors, consider: ```d import std.stdio; struct S{ this(int x){ writeln("constructed"); } this(ref S){ writeln("copied"); } } void foo(S s){} void main(){ S s=2; // ok, actually does `S s=S(2)` S t=s; // ok, actually does `S s=S(s)` // foo(2); // error, refuses to call `foo(S(s))` to convert to `int` foo(s); // ok, calls foo(S(s)) to convert to rvalue } ``` For move constructors, consider: ```d import std.stdio; struct S{ size_t address; this(int){ address=cast(size_t)&this; } this(S){ writeln("rvalue constructor"); } } S foo(bool x){ S s=2,t=2; if(x) return s; else return t; } void main(){ auto s=foo(true); // (no output) auto t=foo(false); // (no output) writeln(s.address," ",cast(size_t)&s); // (two distinct numbers) writeln(t.address," ",cast(size_t)&t); // (two distinct numbers) } ``` So with the compilation strategy that DMD currently uses for this code, it would need to start calling what is currently an rvalue constructor, but at the moment nothing is called. This is a change in behavior. To me this seems desirable, but maybe there is some code out there that does not expect rvalue constructors to be called in this fashion? Perhaps there should be a deprecation period?
Oct 12
On 10/13/24 00:43, Timon Gehr wrote:- Move and copy constructors are indeed a bit special because the compiler may implicitly call them in ways it would not call any other constructor.(Also, it implicitly synthesizes them.)
Oct 12
On 10/13/24 00:43, Timon Gehr wrote:// foo(2); // error, refuses to call `foo(S(s))` to convert to `int`(Should say: "convert to `S`" or "convert from `int`".)
Oct 12