February 11, 2016
On Thursday, 11 February 2016 at 00:54:21 UTC, Ola Fosheim Grøstad wrote:
> On Thursday, 11 February 2016 at 00:32:11 UTC, Matt Elkins wrote:
>> unique owner falls out of scope. This situation occurs a lot for me, and RAII plus move semantics are pretty close to ideal for handling it. Yes, it can be approximated with reference counting, but reference counting has its own downsides.
>
> C++ unique_ptr is a semantically a reference-counting ptr with a max count of 1.

True, but with unique_ptr the max count is enforced by the compiler and can only be subverted by a programmer explicitly choosing to do so -- if that is possible with normal reference counting, I don't know of a way.

Moreover, there is no heap allocation required which may or may not matter for a given use case. Of course there are ways to avoid or mitigate heap allocations for reference-counted pointers, but the point is that unique_ptr has the next best thing to no overhead at all, which allows it to be used in a broader range of contexts.

> In C++ it is a type system issue, and the actual semantics are up to the programmer. In
> D it is just copy and clear, which does extra work and is less flexible _and_ forces
> the copying to happen so you cannot escape it.

Fair point.

February 11, 2016
On Thursday, 11 February 2016 at 01:45:32 UTC, Matt Elkins wrote:
> True, but with unique_ptr the max count is enforced by the compiler and can only be subverted by a programmer explicitly choosing to do so -- if that is possible with normal reference counting, I don't know of a way.

You "subvert" both unique_ptr and shared_ptr by taking the reference of the object and using it directly... Like a borrowed reference in Rust, except it is unchecked (by the compiler).


> Moreover, there is no heap allocation required which may or may not matter for a given use case. Of course there are ways to avoid or mitigate heap allocations for reference-counted pointers, but the point is that unique_ptr has the next best thing to no overhead at all, which allows it to be used in a broader range of contexts.

Yes, I agree, I don't think reference counting is all that important. Although I tend to favour embedding objects rather than using unique_ptr... C++ supports it, but in a rather clunky way. I'd like to see a language that does that really well (sensible initialization tracking of embedded or global objects, so that you can safely delay initialization without resorting to using pointers).

I never really use shared_ptr, but it is quite rich. AFAIK you can swap one pointer for another, you can swap the underlying object without touching the pointers, you can use weak_ptr which checks the presence of the object.

I think shared_ptr is a good thing to have available, but you generally don't have to resort to using it. With a properly structured program you can get a long way with just unique_ptr and your own tailored made structs.

February 11, 2016
On Wednesday, 10 February 2016 at 22:32:54 UTC, Ola Fosheim Grøstad wrote:
> On Wednesday, 10 February 2016 at 20:42:29 UTC, w0rp wrote:
>> Back on the original topic, Scott Meyers often says "std::move doesn't move." It's more like std::rvalue_cast. C++ uses r-value references in order to be able to rip the guts out of objects and put them into other objects.
>
> Well. In C++ "std::move(x)" is just a "static_cast<T&&>(x)" for "T x".
>
> "T&&" references are different from "T&" by acting like T& references when used, but not on overloads. They are primarily for distinguishing between overloads on temporaries in function calls, T&& binds to temporaries. So you use "std::move(x)" to tell the type system that you want it to be cast as a references to a temporary like reference (or rvalue reference).
>
> So that's why constructors with "T&&" are called move constructors (taking stuff from temporaries) and "const T&" are called copy constructors (assuming that the parameter might have a long life on it's own).
>
>> That kind of return value optimisation was the original motivation for r-value references, for when C++98 RVO isn't good enough, from my understanding.
>
> It is for overloading. Why allocate lots of stuff by copying it if you know that the referenced object is about to die anyway? If it is dying we just steal the stuff it is holding.
>
> stack.push(string("hiii")) // we could steal this stuff
> string x("hiii")
> stack.push(x) // we have to copy this stuff
>
>> Maybe someone else will correct me on a point or two there, but that's the understanding of move semantics in D that I have had.
>
> I don't think D has move semantics... It does copying and clearing... The postblit thing looks like a dirty hack to me.

D has move semantics. Deep copies are done with post-blit. Fair enough if you just:

auto foo = bar;

Then it's a shallow copy. The only difference to a "true" move is that bar isn't T.init, but that's easily done with the move function (assuming the struct has a destructor) or manually.


C++:

void foo(Foo); //copy
void foo(Foo&); //by-ref, only lvalues
void foo(Foo&&); //move, only rvalues

D:

void foo(Foo); //copies copiable types, moves non-copiable ones
void foo(ref Foo); //by-ref, only lvalues


In D, the foo(Foo) variant can be called with lvalues as long as they don't @disable this(this). Any type that does isn't copiable so you can only pass rvalues in.

Atila
February 11, 2016
On Thursday, 11 February 2016 at 00:32:11 UTC, Matt Elkins wrote:
> On Wednesday, 10 February 2016 at 20:42:29 UTC, w0rp wrote:
>> [...]
>
> Maybe this is what you are referring to, but the primary use I get out of move semantics (in general, not language-specific) has little to do with performance-on-copy. It is for handling resources which logically aren't copyable, which have a unique owner at all times and which should be cleaned up as soon as unique owner falls out of scope. This situation occurs a lot for me, and RAII plus move semantics are pretty close to ideal for handling it. Yes, it can be approximated with reference counting, but reference counting has its own downsides.
>
> [...]

@disable this(this) should be enough, no?

Atila
February 11, 2016
On Thursday, 11 February 2016 at 14:25:39 UTC, Atila Neves wrote:
> D has move semantics. Deep copies are done with post-blit. Fair enough if you just:
>
> auto foo = bar;
>
> Then it's a shallow copy. The only difference to a "true" move is that bar isn't T.init, but that's easily done with the move function (assuming the struct has a destructor) or manually.

*blank stare*

> C++:
>
> void foo(Foo); //copy
> void foo(Foo&); //by-ref, only lvalues
> void foo(Foo&&); //move, only rvalues

In modern generics-oriented C++ I would say:

void foo(T); // by value - and probably not what you want
void foo(const T&) // copy semantics overload
void foo(T&&) // move semantics overload

Please keep in mind that C++ do perfect forwarding of those rvalue references when you pass it down a call chain.

February 11, 2016
On Thursday, 11 February 2016 at 14:38:24 UTC, Ola Fosheim Grøstad wrote:
> On Thursday, 11 February 2016 at 14:25:39 UTC, Atila Neves wrote:
>> D has move semantics. Deep copies are done with post-blit. Fair enough if you just:
>>
>> auto foo = bar;
>>
>> Then it's a shallow copy. The only difference to a "true" move is that bar isn't T.init, but that's easily done with the move function (assuming the struct has a destructor) or manually.
>
> *blank stare*

Err... ok.

>> C++:
>>
>> void foo(Foo); //copy
>> void foo(Foo&); //by-ref, only lvalues
>> void foo(Foo&&); //move, only rvalues
>
> In modern generics-oriented C++ I would say:
>
> void foo(T); // by value - and probably not what you want

It depends. For small structs, it is. And in some cases the compiler can elide the copy.

> void foo(const T&) // copy semantics overload
> void foo(T&&) // move semantics overload

I forgot the const. It doesn't change my point.

> Please keep in mind that C++ do perfect forwarding of those rvalue references when you pass it down a call chain.

No it doesn't. It _allows_ you to perfect forward, as long as you remember to use `std::forward`. And in that case, they're not really rvalue references, they're forwarding references (what Scott Meyers initially called universal references).

The only issue that I know of with D's approach is that, if you want to pass by ref for efficiency reasons, then you can't pass an rvalue in. It's never been a problem for me.


Atila


February 11, 2016
On Thursday, 11 February 2016 at 16:31:03 UTC, Atila Neves wrote:
> On Thursday, 11 February 2016 at 14:38:24 UTC, Ola Fosheim Grøstad wrote:
>> On Thursday, 11 February 2016 at 14:25:39 UTC, Atila Neves wrote:
>>> D has move semantics. Deep copies are done with post-blit. Fair enough if you just:
>>>
>>> auto foo = bar;
>>>
>>> Then it's a shallow copy. The only difference to a "true" move is that bar isn't T.init, but that's easily done with the move function (assuming the struct has a destructor) or manually.
>>
>> *blank stare*
>
> Err... ok.

I don't see how D's parameter semantics can be called move semantics, when you essentially can emulate it in C++ without using C++'s move semantics?

>> void foo(const T&) // copy semantics overload
>> void foo(T&&) // move semantics overload
>
> I forgot the const. It doesn't change my point.

The point is of course that you use (const T&) instead of copying, so you don't have to deal with the constructor/destructor overhead?

> No it doesn't. It _allows_ you to perfect forward, as long as you remember to use `std::forward`. And in that case, they're not really rvalue references, they're forwarding references (what Scott Meyers initially called universal references).

Of course you have to use std::forward, that follows from what I said further up about how "T&&" parameters act when used.

February 13, 2016
On Wednesday, 3 February 2016 at 15:56:48 UTC, Ola Fosheim Grøstad wrote:
> On Wednesday, 3 February 2016 at 15:44:25 UTC, Sönke Ludwig wrote:
>> seems like pretty clear semantics. And in C++ you'd have the same situation once somefunction decides to move/swap the value somewhere else before throwing an exception.
>
> Well, you can always move it back or wait with the move.
>
> Also, std.move may end up being inefficient when you have a complicated resource holder. Since the work is done before calling the function the optimizer may struggle with getting rid of the work.

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.

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.  Personally, I think D's move semantics are actually clearer and easier to get right.

It is somewhat unfortunate that you cannot provide the strong exception guarantee for a function when you move arguments into it, though, but the semantics are pretty clear and easy to 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.

Lars
February 13, 2016
On Thursday, 11 February 2016 at 00:32:11 UTC, Matt Elkins wrote:
>
> Maybe this is what you are referring to, but the primary use I get out of move semantics (in general, not language-specific) has little to do with performance-on-copy. It is for handling resources which logically aren't copyable, which have a unique owner at all times and which should be cleaned up as soon as unique owner falls out of scope. This situation occurs a lot for me, and RAII plus move semantics are pretty close to ideal for handling it. Yes, it can be approximated with reference counting, but reference counting has its own downsides.

This is my primary use case for move semantics too, in both C++ and D.  Using noncopyable structs for this is perfect, because you can keep them on the stack and avoid heap allocation and indirection if you don't need to move them around a lot.  If you do move them often enough that D's copy-and-clear semantics become a performance issue, just stick them in something like std.typecons.Unique or std::unique_ptr.  Finally, if you need to have multiple owners/references, put them in a std.typecons.RefCounted or std::shared_ptr.

Best of all worlds.

Lars


February 13, 2016
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.

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.

>  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.

> 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? Or can the type system help you?