February 13, 2016
On Saturday, 13 February 2016 at 12:14:43 UTC, Ola Fosheim Grøstad wrote:
> On Saturday, 13 February 2016 at 09:11:06 UTC, Lars T. Kyllingstad wrote:
>> In my experience, in the vast majority of cases a C++ move operation boils down to a memberwise copy (of value types) or copy-and-reset (of reference types).  With the extra logic and program flow that is sometimes involved in move construction and move assignment, I suspect that a straightforward double memcpy as it is done in D will be almost as performant or moreso most of the time.
>
> No? Not in the libraries I write. A move typically just means copying 1-4 64 bit values and setting a single 64 bit value in the source. Usually written as an inline function.

Not knowing anything about the libraries you write, it's hard to argue with that.  But I agree that given that you are in control of all the code AND can make the move ctor/assignment available for inlining (AND are an experienced programmer), then yes, you can most definitely get better performance with C++'s move() than with D's move().

But consider the more general case where you have an object of type 'struct A', which is embedded in an object of type 'struct B', which is again embedded in an object of type 'struct C', and so on, and where A, B, and C are perhaps in separate libraries or for some other reason their move ctors/assigments cannot be inlined.  Then, you are looking at multiple levels of function calls and you are also at the mercy of whoever wrote their move code.

In D the cost of a move is very predictable and should be performant enough for most use cases.  And for the ones where it absolutely isn't, I'm sure making a custom solution is feasible.


> Or nothing, in the case where the logic does not end up with a move, something which D cannot represent with the same semantic distinction.
>
>> Add to that the fact that a lot of programmers out there will implement move construction in terms of move assignment -- which makes it a default construction PLUS move -- and move assignment in terms of swap -- i.e., three moves -- for the sake of DRY.
>
> Huh? Move assignments is no different from move construction, except you have to release the existing value if the receiving object isn't empty. Constructing an empty resource owner object usually just means setting a single field to zero, which is inlined and removed if it is followed by an assignment.

What I meant is that you will find a lot of C++ code out there, written by well-meaning programmers, that looks like this:

class C
{
    C(C&& other)
    {
        operator=(std::move(other));
    }
    // and/or
    C& operator=(C&& other)
    {
        swap(*this, other);
        return *this;
    }
};

Here, you have unnecessary construction of C's members in the constructor which may or may not be optimised away before the assignment.  Furthermore, you have an unnecessary number of moves in the assignment operator -- plus the potential drawbacks of deferred release of the resource.

>>  Personally, I think D's move semantics are actually clearer and easier to get right.
>
> But I don't think D has move semantics. I don't think it makes for correctness for resource ownership.

I'm not sure what you mean by "has move semantics" here.  It does not have C++'s move semantics, no, but I would say D has its own move semantics.  It has a move() function that transfers raw state between objects, and D structs are supposed to be designed so they are movable by means of raw bit transfer, allowing the compiler and GC to move them around as it sees fit.  But maybe I'm missing something?


>> explain to newbies:  If you use std.move() on something it definitely gets moved.  In C++, if you use std::move() on something it may or may not be moved; it depends on the recipient of the move.
>
> No? With D's std.move() the resource can be destroyed or get into an inconsistent state if the caller does it wrong?

I guess this is what I don't understand.  How and when does that happen?

Lars
February 13, 2016
On Saturday, 13 February 2016 at 17:47:54 UTC, Lars T. Kyllingstad wrote:
> Not knowing anything about the libraries you write, it's hard to argue with that.  But I agree that given that you are in control of all the code AND can make the move ctor/assignment available for inlining (AND are an experienced programmer),

C++ does indeed put the burden on the library programmer and is not a good language for "non-professional" use. But it is flexible by providing the mechanisms in the type system rather than an opinionated solution. (Of course, parts of the C++ standard library is opinionated.)

> cannot be inlined.  Then, you are looking at multiple levels of function calls and you are also at the mercy of whoever wrote their move code.

Well, I primarily use move semantics for ownership, like owning resources in the GPU, files system, memory etc. So it usually is 1 or 2 levels.


> Here, you have unnecessary construction of C's members in the constructor which may or may not be optimised away before the assignment.

Well, doing a swap would break the expectations for assignment...

>  Furthermore, you have an unnecessary number of moves in the assignment operator -- plus the potential drawbacks of deferred release of the resource.

I don't understand what you mean by unnecessary moves? std::move/std::forward are just type casting so they don't result in code...

> I'm not sure what you mean by "has move semantics" here.  It does not have C++'s move semantics, no, but I would say D has its own move semantics.  It has a move() function that transfers raw state between objects, and D structs are supposed to be designed so they are movable by means of raw bit transfer, allowing the compiler and GC to move them around as it sees fit.  But maybe I'm missing something?

Well, but that is like saying that C++03 also had move semantics. There is nothing special about D's move(), it's just a library function?

>> No? With D's std.move() the resource can be destroyed or get into an inconsistent state if the caller does it wrong?
>
> I guess this is what I don't understand.  How and when does that happen?

The std.move() actually does a copy, then copy the init value to the original. If something happens that prevents the value from being preserved the object will be destroyed by the destructors. I.e. an exception.

And worse, if you have back pointers to it, it will end up being inconsistent. There is no way the type system can prevent back pointers without preventing D from being usable as a language. Since you no longer have the original object... shit can happen. In C++ you can set a mutex in the object and fix things because you have the full object. So if someone tries to follow the back pointer the mutex will block. You can probably come up with many other scenarios. "postblit" does not fix this (not a very elegant solution IMO).

So, C++ gives the library author control by having "move" be part of the type system that essentially does nothing else than applying some constraints. Having "move" as an action is both limiting and potentially flawed, since the D compiler does not do anything to ensure correctness. If "move" is an action, rather than a type system constraint, then it should be backed up with semantic analysis IMO.

February 13, 2016
On Saturday, 13 February 2016 at 19:25:37 UTC, Ola Fosheim Grøstad wrote:
>> I'm not sure what you mean by "has move semantics" here.  It does not have C++'s move semantics, no, but I would say D has its own move semantics.  It has a move() function that transfers raw state between objects, and D structs are supposed to be designed so they are movable by means of raw bit transfer, allowing the compiler and GC to move them around as it sees fit.  But maybe I'm missing something?
>
> Well, but that is like saying that C++03 also had move semantics. There is nothing special about D's move(), it's just a library function?

D "guarantees" NRVO which is what enables its move semantics, C++ did/does not.

Quotes because IIRC(?) it used to be part of the spec and it isn't anymore, I don't think Walter or Andrei have addressed this yet so I'm not sure if it's intended.
February 13, 2016
On Saturday, 13 February 2016 at 20:11:45 UTC, rsw0x wrote:
> D "guarantees" NRVO which is what enables its move semantics, C++ did/does not.
>
> Quotes because IIRC(?) it used to be part of the spec and it isn't anymore, I don't think Walter or Andrei have addressed this yet so I'm not sure if it's intended.

By NRVO I assume you mean:

https://en.wikipedia.org/wiki/Return_value_optimization

All the common C++ compilers do RVO, but you don't need that to implement move semantic-like behaviour in C++03.

I wouldn't call RVO move semantics at all...
February 13, 2016
On Saturday, 13 February 2016 at 20:24:12 UTC, Ola Fosheim Grøstad wrote:
> On Saturday, 13 February 2016 at 20:11:45 UTC, rsw0x wrote:
>> D "guarantees" NRVO which is what enables its move semantics, C++ did/does not.
>>
>> Quotes because IIRC(?) it used to be part of the spec and it isn't anymore, I don't think Walter or Andrei have addressed this yet so I'm not sure if it's intended.
>
> By NRVO I assume you mean:
>
> https://en.wikipedia.org/wiki/Return_value_optimization
>
> All the common C++ compilers do RVO, but you don't need that to implement move semantic-like behaviour in C++03.
>
> I wouldn't call RVO move semantics at all...

It's not move semantics, it enables move semantics.
Yes, C++ compilers do it(Walter 'invented' it, fyi) but C++ doesn't guarantee it. D (is supposed) to) guarantee it, which enables move semantics.

i.e, see https://issues.dlang.org/show_bug.cgi?id=5777
February 13, 2016
On Saturday, 13 February 2016 at 20:39:22 UTC, rsw0x wrote:
> It's not move semantics, it enables move semantics.
> Yes, C++ compilers do it(Walter 'invented' it, fyi) but C++ doesn't guarantee it. D (is supposed) to) guarantee it, which enables move semantics.

I understand what you mean. You mean construction of read/write protected objects, which you can override with dedicated functions with special privileges. But you can do that in old C++ as well... You only need to create your own reference type.

Of course, there is not much to invent as the common C paradigm for objects has always been that kind of initialization which RVO "emulates":

  data_t data; initialize_data(&data);

The primary difference is that C does not provide any protection for the data. The issue in C++ is that it is not an optimization, as it breaks the language semantics (which the standard now allows).

February 13, 2016
On Saturday, 13 February 2016 at 19:25:37 UTC, Ola Fosheim Grøstad wrote:
> On Saturday, 13 February 2016 at 17:47:54 UTC, Lars T. Kyllingstad wrote:
>> [...]
>
> C++ does indeed put the burden on the library programmer and is not a good language for "non-professional" use. But it is flexible by providing the mechanisms in the type system rather than an opinionated solution. [...]

D is all about opinionated solutions. :)  In fact, I would go so far as to say that's what sets D apart from C++, and mostly in a good way.


> [...]
>
>> Here, you have unnecessary construction of C's members in the constructor which may or may not be optimised away before the assignment.
>
> Well, doing a swap would break the expectations for assignment...

Whose expectations?  The formal expectation, as per the C++ standard, is that the moved-from object be left in a "valid but unspecified state".  Basically, as long as it is safe to destroy or reassign to the moved-from object, you're good.

I hope this is not coming across as me endorsing the practice of implementing move assignment in terms of swap, because I don't.  But it *is* a rather common practice, enough so that Scott Meyers felt the need to write an article about it:

http://scottmeyers.blogspot.no/2014/06/the-drawbacks-of-implementing-move.html


>>  Furthermore, you have an unnecessary number of moves in the assignment operator -- plus the potential drawbacks of deferred release of the resource.
>
> I don't understand what you mean by unnecessary moves? std::move/std::forward are just type casting so they don't result in code...

A swap is three moves -- actual moves.


>> I'm not sure what you mean by "has move semantics" here.  It does not have C++'s move semantics, no, but I would say D has its own move semantics.  It has a move() function that transfers raw state between objects, and D structs are supposed to be designed so they are movable by means of raw bit transfer, allowing the compiler and GC to move them around as it sees fit.  But maybe I'm missing something?
>
> Well, but that is like saying that C++03 also had move semantics. There is nothing special about D's move(), it's just a library function?

What is special is D's requirement that structs be movable by a raw bit blit, which again enables our particular library implementation of move().

C++ has no such requirement; for example it is perfectly OK for an on-stack C++ object to contain a pointer to itself.  A D-like move() on such an object would just produce mayhem.


>>> No? With D's std.move() the resource can be destroyed or get into an inconsistent state if the caller does it wrong?
>>
>> I guess this is what I don't understand.  How and when does that happen?
>
> The std.move() actually does a copy, then copy the init value to the original. If something happens that prevents the value from being preserved the object will be destroyed by the destructors. I.e. an exception.

If you are still talking about this case:

    fun(move(someResource));

where fun() throws, like Sönke I don't have a big problem with the resource being lost there.  Given D's semantics, this is what I would expect.  And in D, at least you *know* it is lost, and it is easy to understand and explain to users.  In C++ you have no idea whether the resource is lost -- it depends on when the actual move operation happens and when the exception is thrown.


> And worse, if you have back pointers to it, it will end up being inconsistent. There is no way the type system can prevent back pointers without preventing D from being usable as a language. Since you no longer have the original object... shit can happen. In C++ you can set a mutex in the object and fix things because you have the full object. So if someone tries to follow the back pointer the mutex will block. You can probably come up with many other scenarios. "postblit" does not fix this (not a very elegant solution IMO).

Still not following you.  Postblit is not involved in a move at all -- that's what makes it a move.

If an exception is thrown, the object's destructor is called, and it would be the destructor's responsibility to break all ties to other objects.


> [...]

February 13, 2016
On Saturday, 13 February 2016 at 21:41:06 UTC, Lars T. Kyllingstad wrote:
> D is all about opinionated solutions. :)  In fact, I would go so far as to say that's what sets D apart from C++, and mostly in a good way.

D1 was all about opinionated ready-made builtin solutions, but D2 is supposedly meant to support generic programming and then the responsibility of "creating opinions" should be moved out of the language and into libraries.


> Whose expectations?  The formal expectation, as per the C++ standard, is that the moved-from object be left in a "valid but unspecified state".  Basically, as long as it is safe to destroy or reassign to the moved-from object, you're good.

Hmm, do you have a reference (URL) to the "valid but unspecified state" part? I'm not quite sure what that refers to.


> I hope this is not coming across as me endorsing the practice of implementing move assignment in terms of swap, because I don't.  But it *is* a rather common practice, enough so that Scott Meyers felt the need to write an article about it:

I've never heard of it, and never thought it would be a good idea. Are you sure this is common?


> A swap is three moves -- actual moves.

If you are talking std::swap, probably. Never use it, so don't know if there is any measurable overhead.

If you are talking about the microcode in the CPU, then it typically takes 2 loads and 2 stores to swap two pointers on the heap, and the loads and stores can execute in parallel... So performance wise, not a big deal. But with debugging/consistency in mind you should set the source to nullptr instead.

I know programmers talk alot about swap being implemented as

tmp = a
a = b
b = a

But that is actually how it is specified it source code, not how it is implemented in running code. In the CPU it goes like this:

reg1 = load a; reg2 = load b
b = store reg1; a = store reg2

Setting it to null would be almost the same.

reg1 = load a; reg2 = 0
b = store reg1; a = store reg2

Unless you use special commands and zero out the entire cacheline of "a" you still get the same amount of cache-misses as well.


> What is special is D's requirement that structs be movable by a raw bit blit, which again enables our particular library implementation of move().
>
> C++ has no such requirement; for example it is perfectly OK for an on-stack C++ object to contain a pointer to itself.  A D-like move() on such an object would just produce mayhem.

Yes, but it isn't enforced by the compiler by static analysis, is it?  So D has no particular advantage to C++ for an object that is designed to be movable.


> and it is easy to understand and explain to users.  In C++ you have no idea whether the resource is lost -- it depends on when the actual move operation happens and when the exception is thrown.

Well, you do, if you implement exception safe RAII, which you should. RAII ensures that it will be released when the owner object is destructed.


> Still not following you.  Postblit is not involved in a move at all -- that's what makes it a move.

Well, if you have a back pointer that is part of the invariant for the type, then neither move or copy work as expected. In C++ you have the address of the source object and can either modify or change the associated data-structure that provide back pointers (e.g. a global hash with pointers to the struct, in C++ you can change these pointers to point to the new location/add another entry). AFAIK this is not possible in D without adding an indirection.


February 13, 2016
On Saturday, 13 February 2016 at 22:42:34 UTC, Ola Fosheim Grøstad wrote:
> Hmm, do you have a reference (URL) to the "valid but unspecified state" part? I'm not quite sure what that refers to.

Nevermind, I don't need a reference. I think what you refer to is just a statement that says that you can do whatever you want, but the destructor will run so the object must be in a valid state?

> tmp = a
> a = b
> b = a

Duh, "b=tmp"...

February 14, 2016
On Saturday, 13 February 2016 at 22:42:34 UTC, Ola Fosheim Grøstad wrote:
> On Saturday, 13 February 2016 at 21:41:06 UTC, Lars T. Kyllingstad wrote:
>> Whose expectations?  The formal expectation, as per the C++ standard, is that the moved-from object be left in a "valid but unspecified state".  Basically, as long as it is safe to destroy or reassign to the moved-from object, you're good.
>
> Hmm, do you have a reference (URL) to the "valid but unspecified state" part? I'm not quite sure what that refers to.

I know you said afterwards you didn't need a reference, but I'll give you one anyway. :)  That is the formal requirement for C++ standard library types; see sec. 17.6.5.15 [lib.types.movedfrom] of the C++ specification.

But I agree that, for the most part, one would expect that the moved-from object holds *no* resource and that the resource previously held by the target object has been released.


>> I hope this is not coming across as me endorsing the practice of implementing move assignment in terms of swap, because I don't.  But it *is* a rather common practice, enough so that Scott Meyers felt the need to write an article about it:
>
> I've never heard of it, and never thought it would be a good idea. Are you sure this is common?

Pretty sure, but off the top of my head I can't give you too many concrete examples beyond the Meyers article I linked to, Stack Overflow questions, and one particular well-known and respected library (ZeroMQ) where I recently ran into it.


>> A swap is three moves -- actual moves.
>
> If you are talking std::swap, probably. Never use it, so don't know if there is any measurable overhead.

I was, yes.


> If you are talking about the microcode in the CPU, then it typically takes 2 loads and 2 stores to swap two pointers on the heap, and the loads and stores can execute in parallel... So performance wise, not a big deal. But with debugging/consistency in mind you should set the source to nullptr instead.
>
> I know programmers talk alot about swap being implemented as
>
> tmp = a
> a = b
> b = a
>
> But that is actually how it is specified it source code, not how it is implemented in running code. In the CPU it goes like this:
>
> reg1 = load a; reg2 = load b
> b = store reg1; a = store reg2
>
> Setting it to null would be almost the same.
>
> reg1 = load a; reg2 = 0
> b = store reg1; a = store reg2
>
> Unless you use special commands and zero out the entire cacheline of "a" you still get the same amount of cache-misses as well.

For your lowest-level resource owners, I guess that is the case.  But if a and b are largeish compound types that don't fit in a register, that's not the case, right?  Or can the optimiser deal with this in a good way too?


> Well, if you have a back pointer that is part of the invariant for the type, then neither move or copy work as expected. In C++ you have the address of the source object and can either modify or change the associated data-structure that provide back pointers (e.g. a global hash with pointers to the struct, in C++ you can change these pointers to point to the new location/add another entry). AFAIK this is not possible in D without adding an indirection.

So what you're saying that a particular kind of designs/patters can not be safely combined with D's standard move mechanism.  That is of course very unfortunate, but I guess it can be worked around?  I thought you were implying that simply using move() on any struct could potentially mess it up...