DIP 1014

Stanislav Blinov stanislav.blinov at gmail.com
Wed Oct 3 17:43:08 UTC 2018


On Wednesday, 3 October 2018 at 15:33:00 UTC, Shachar Shemesh 
wrote:
> On 03/10/18 17:29, Stanislav Blinov wrote:
>>> OMG, that's so simple!!! Why didn't I think of it?
>>>
>>> Oh wait, I did.
>> 
>> Now I see why sometimes your posts are greeted with hostility.
>
> Yes. I am actually sorry about that. I was responding to your 
> assumption that I'm wrong. Had your post been phrased as "why 
> didn't you", instead of "you're wrong wrong wrong" I wouldn't 
> have responded that way.
>
> Like I said, I am sorry.

I am sorry as well since I wasn't clear in my initial post.

> > Allow me to further illustrate with something that can be
> written in D > today:
>
> I am not sure what you were trying to demonstrate, so instead I 
> wanted to see if you succeeded. I added the following to your 
> Tracker struct:
>
>      ~this() {
>         writefln("%s destructed", &this);
>         assert(counter is null || counter is &localCounter);
>     }
>
> I.e. - I am asserting if a move was not caught. The program 
> fails to run on either ldc or dmd. To me, this makes perfect 
> sense as for the way D is built. In essence, opAssign isn't 
> guaranteed to run. Feel free to build a struct where that 
> assert passes to convince me.

That's a slightly different issue here.
Look at the output. The operator is being run, it can't *not* 
run, unlike postblit (ironically, right now it doesn't run on 
fixed-size arrays though). In fact, as soon as you define a 
destructor, the compiler will generate a by-value opAssign if you 
haven't defined one.
That's a separate problem. Currently, presence of a destructor 
makes the compilers generate different code, because it cannot 
elide destruction of arguments, because explicit move semantics 
do not exist in the language. That's why I haven't included a 
destructor in the example to begin with.

> Here is the flaw in your logic:
>
>     void opAssign(Tracker rhs)
>
> rhs is passed by value. This means that already at the point 
> opAssign is called, rhs *already* has a different address than 
> the one it was passed in with.

Currently that is only true if you define a destructor. That 
would not be true, however, if a move hook in any form existed in 
the language. That was my point.
I only used opAssign as something resembling the supposed new 
behavior, not as a "look, it already works". In the presence of a 
move hook, 'rhs' would first have to pass through that hook, 
which will not take destructors into account at all.
Consider your own DIP: what you're suggesting is the ability to 
take the address of the original when a move is taking place. My 
example shows that in the simplest case even today, address of 
the original is already the address of the argument. Except it 
cannot be enforced in any way right now. A move hook will have to 
enforce that, as it will have to be called for every move.

> I did not follow your logic on why this isn't so, but I don't 
> see how you can make it not so without changing the ABI quite 
> drastically.

The changes are literally the same as the ones you're proposing:

"When moving a struct's instance, the compiler MUST call 
__move_post_blt giving it both new and old instances' addresses."

That is the same that would have to happen with this(typeof(this) 
rhs), where &this is the address of new instance, and &rhs is the 
address of old instance, but there's no need for opPostMove then. 
I guess what I should've said from the start is that the 
semantics you're proposing fit nicely within one special 
function, instead of two.

this(typeof(this)), of course, would need to be special in the 
ABI, but again, that's one special function instead of two.

Let's take a step back for a moment and look at what should 
actually be happening for this hook to work (which you briefly 
mention in the DIP):

1. The compiler constructs the value. In your case, it constructs 
two: the original and the new one. In my case, it constructs the 
original and then passes it over to the move ctor (one blit 
potentially avoided).
2. It calls the hook (move ctor).
3. In your case, it calls the opPostMove.
4. In any case, it *doesn't* destruct the original. Ever. The 
alternative would be to force the programmer to put the original 
back into valid state, and suddenly we're back to C++ with all 
it's pleasantries.

That last part is quite different from the current model, in 
which the compiler always destructs function arguments. That's 
why my example fails when a destructor is present.

The other thing to note (again something that you mention but 
don't expand on), and that's nodding back to my comment about 
making move() and emplace() intrinsics, is that creating such a 
hook *will* invalidate current behavior of move(). Which is 
perhaps more easily fixed with your implementation, actually, 
*except* for the part about eliding destruction. Unions are 
unreliable for that unless we also change the spec that talks 
about them.
But IMHO, it's something that should be fixed by not making these 
facilities built into the language.


More information about the Digitalmars-d mailing list