News for hackers. Stuff that matters.
Re:You can't Rust that by pcwalton on Saturday March 31, 2018 @02:18PM (#16724409)
I really like the way you captured one of the fundamental differences between Rust and C++ as "Things Move". That's an interesting way to summarize it that I hadn't really considered before—and I designed a lot of that system :)
Re:You can't Rust that by jnordwick on Saturday March 31, 2018 @02:24PM (#16724436)
Is is even remotely true though? C++ probably moves things around more than rust, and I thought rust would want to reduce cache churn. It isn't like GC were things magically change locations.

I'm not really sure I understand what he's getting at with that description.

Re:You can't Rust that by oconnor663 on Saturday March 31, 2018 @04:16PM (#16724965)
> I thought rust would want to reduce cache churn

I think the main way Rust reduces cache churn, is that it makes it safer to pass references/pointers around instead of making copies. For example, if I have f(char*) in C, I might be worried about what f is doing with that pointer. It might save it somewhere, or try to free it, or who knows what. In C++, I might prefer to write f(std::string) to avoid those worries, but that comes at the cost of unnecessarily copying the string. (You could pass it by reference to avoid the copy, but then you still have to worry about f saving the reference.) In Rust, I can safely write f(&str), and I know f can't do anything dirty with that reference.

Re:You can't Rust that by jstimpfle on Saturday March 31, 2018 @04:46PM (#16725099)
> If I have f(char*) in C, I might be worried about what f is doing with that pointer. It might save it somewhere, or try to free it, or who knows what

FWIW my assumption always is that it's "a borrow". I.e., the called function should not make any assumptions how the memory was allocated, what is its lifetime, or even try to free it or keep it in an associated datastructure that persists the lifetime of the function. The only thing it should normally assume is that it points to valid memory.

In my experience the instances where ownership moves across function boundaries are far and far between, and can easily be documented, often simply by giving it an appropriate name.

Re:You can't Rust that by the_mitsuhiko on Saturday March 31, 2018 @04:48PM (#16725109)
> I.e., the called function should not make any assumptions how the memory was allocated, what is its lifetime

It needs to at least make the assumption that the memory will not be invalidated while the function did not return. In the presence of multi threading that in itself might already be surprisingly hard to guarantee sometimes. Also typically functions make assumptions about the immutability of things in such cases which are often just not true.

Re:You can't Rust that by jstimpfle on Saturday March 31, 2018 @05:40PM (#16725313)
Problems with multithreading and "perceived immutability" are two more things that can largely be ruled out with a clear dataflow architecture. For example, I try to not have "cycles" such that a function call overwrites a memory location that was a (pointer) argument to the caller. I don't think that should happen - where did the called function get the pointer from without the caller knowing?

An architecture with global data tables instead of OOP helps here, because it has much lower need for pointer arguments of unclear origin. I also agree very much with your approach "handles instead of pointers" (if by handles you mean integer array indices). Indices for example trivially survive reallocation of a dynamically growable array.

Re:You can't Rust that by amarkov on Sunday April 01, 2018 @02:15PM (#16729642)
Sure, but isn't that the point for Rust? You can rule out huge classes of problems with a clear dataflow architecture - thus, the language should enforce one, rather than requiring programmers to hand-roll one correctly.
Re:You can't Rust that by jstimpfle on Sunday April 01, 2018 @10:26PM (#16731847)
I've never used Rust, but my impressionn is it's quite constraining... The syntax alone turns me off.
Re:You can't Rust that by klmr on Monday April 02, 2018 @06:56AM (#16733570)
> FWIW my assumption always is that it's "a borrow".

But for how long? Forget about multithreading for a moment, but can my function store the pointer for later use? This is incredibly common in OOP, and dangling pointers/references are one of the (if not the) top sources of bugs in C and C++. And unlike other sources of bugs, there’s no good fundamental remedy against it.

Re:You can't Rust that by jstimpfle on Monday April 02, 2018 @07:25AM (#16733672)
> But for how long?

As I wrote, "lifetime of the function". Maybe that was poorly worded - of course I mean "until the function call returns". (I think "borrow" is universally understood in this way).

That means, again, don't assume you are allowed to keep the pointer argument in an associated datastructure. In general we don't know where the data is coming from, how it was allocated, how or when it will be freed, etc. It's just that: data.

And as I wrote in the other comment, don't do OOP. It's wrong. I think it has had a good twenty years of hype to prove any merits, and failed to do so. Yes, OOP without a garbage collector cannot be done reliably because the pointers will kill you. But that's not an indication that pointer management is impossible. It's an indication that we shouldn't do OOP. Even in a GCed language it's still a pain to maintain all these redundant links, and to write algorithms that collect the data that was littered in a thousand places and is accessible only through a hard to understand object interface.

There are much simpler ways to organize a program. Pointer management can be pretty easy when the project structure is right, and we focus on the data instead an overly bureaucratic object graph of funky objects that do almost nothing in the name of "reusability".

Re:You can't Rust that by klmr on Monday April 02, 2018 @09:07AM (#16734145)
It’s not that simple. First off, OOP is actually useful if not abused. Even in modern C++ it definitely has its place. And secondly, you run into the same problem outside the limited scope of OOP. Every large, well-designed library provides its own solution to this problem of “holding onto” a value and extending its lifetime, if necessary. Obviously I agree with you that a bare pointer shouldn’t ever be (ab)used for this purpose but as mentioned that’s easier said than done, since the alternatives aren’t trivial. Lifetime extension really is difficult.
Re:You can't Rust that by jstimpfle on Monday April 02, 2018 @11:10AM (#16735141)
My general approach is to divide a project into modules which hold their data in global tables (typically a handful of dynamically allocated arrays). The data's lifetime is managed by that module and that module only. Most of the data never needs to be exposed to other modules, and most of the functionality related to that data is encapsulated in the module.

Other modules use integer handles to access data. They might convert the integer handle to a pointer and iterate over the data with the help of the module, but these pointers are not stored. The module's interface has very clear and simple rules which commands from its API invalidate those pointers (because they might trigger reallocation). But of course only few commands do invalidate pointers, and they are typically not interleaved with the other commands. And typically getting pointers is not even needed, because the relevant functionality is implemented in the module that owns the data.

There are generally no pointers inside the data (perhaps a few const char pointers to static or quasi-static data) and most intra-module links are realized using integer indexes, just like it's done in relational databases.

So in a way that is actually "object-oriented", but not in the usual sense of creating a million instances of some class that are linked in a complicated way. Every module is a kind of object. So the number of these objects is typically static. All the linking between these objects is simply done by: the linker :-)

Re:You can't Rust that by staticassertion on Saturday March 31, 2018 @02:41PM (#16724508)
They're talking about move semantics, not actual memory moving around.

In c++ you get copy semantics by default, requiring std::mov to get move semantics. In rust it is the opposite.

Re:You can't Rust that by masklinn on Saturday March 31, 2018 @02:58PM (#16724582)
> In c++ you get copy semantics by default, requiring std::mov to get move semantics.

And even then:

1. std::move only creates an rvalue reference, it doesn't necessarily move anything (not only does that depend on the presence of a move ctor & the receiver's arguments, there's still no requirement that anything actually moves)

2. a move in the C++ sense "empties" the object, the object is still there and accessible in "some valid but otherwise indeterminate state", objects can even fully specify the exact state they're in when moved-from (unique_ptr does)

Re:You can't Rust that by bergesenha on Saturday March 31, 2018 @04:28PM (#16725018)
1. I thought the cast to an rvalue reference would make overloads taking rvalue references be chosen during overload resolution. Since this is deterministic I thought whether something is moved or not would be quite guaranteed
Re:You can't Rust that by jeremyjh on Saturday March 31, 2018 @05:07PM (#16725179)
I think he just means the move constructor is not guaranteed to move anything. It is only expected to.
Re:You can't Rust that by snuxoll on Saturday March 31, 2018 @11:12PM (#16726500)
The rule of three/five is probably the most tedious thing to deal with in modern C++. Assuming all your member variables are C++ types it's not TERRIBLE, you should be able to handle everything in the initialization list (using std::move for the move constructor) - but it's still easy to forget to initialize a member.

This is one area where Rust really makes life much easier.

Re:You can't Rust that by Too on Sunday April 01, 2018 @01:58AM (#16726973)
Use rule of zero as far as possible. All the tedious stuff that comes with rule of five should be isolated to very few components in your code.
Re:You can't Rust that by jcelerier on Sunday April 01, 2018 @10:14AM (#16728371)
> 1. I thought the cast to an rvalue reference would make overloads taking rvalue references be chosen during overload resolution. Since this is deterministic I thought whether something is moved or not would be quite guaranteed

well, take for instance this function :

    my_struct eat_pointer(std::unique_ptr<int>&& x) {
      if(x && *x > 0)
        return my_struct{std::move(x)};
      return my_struct{};
    }

    auto my_pointer = std::make_unique<int>(-456);
    auto res = eat_pointer(std::move(my_pointer));
    // at this point my_pointer hasn't moved a bit
    
sure, this code sucks, but that's always a possibility
Re:You can't Rust that by bergesenha on Sunday April 01, 2018 @07:12PM (#16731077)
Thanks for the example, I understand. I missunderstood the poster above. I've come across quite a few coworkers who believe that move is something that happens on the compilers whim as part of some optimization and incorrectly assumed the same of the post i replied to.
Re:You can't Rust that by saagarjha on Saturday March 31, 2018 @02:45PM (#16724524)
> C++ probably moves things around more than rust

I don't think so, actually. As far as I'm aware moves are only done when explicitly asked for, because any implicit move would have the consequence of breaking references and being unsafe in general if you're not careful.

Re:You can't Rust that by wilun on Saturday March 31, 2018 @07:47PM (#16725803)
> As far as I'm aware moves are only done when explicitly asked for

Certainly not. Move are attempted (in the sense that the move constructor/operator= will be called if it exists) from rvalue references. Those are for example all temporaries. That has let accelerate all C++ programs using std objects by simply recompiling them (with the new versions of the objects supporting the move while it did not exist in the old versions)

Re:You can't Rust that by saagarjha on Saturday March 31, 2018 @07:55PM (#16725828)
Well, it makes sense to move temporaries. There's no point in copying them again if the original won't be used.
Re:You can't Rust that by jcelerier on Sunday April 01, 2018 @10:15AM (#16728378)
> As far as I'm aware moves are only done when explicitly asked for

no, every time you have a function returning a temporary you can have a move, e.g.

    std::vector<int> myfun();
    
    struct myclass { myclass(std::vector<int> v); };
here if you do

    myclass c{myfun()}; 
there can be a move
Re:You can't Rust that by the_mitsuhiko on Saturday March 31, 2018 @02:31PM (#16724464)
> I'm not really sure I understand what he's getting at with that description.

I'm getting at that an object in C++ is generally immovable. If it does move (which is new in C++11 i think?) a move ctor is invoked. Rust does not have a move ctor, it does not let you do things that would prevent moving an object in the first place (at least currently).

Re:You can't Rust that by gmueckl on Saturday March 31, 2018 @02:53PM (#16724559)
A move in C++ is a special construct where a new object is allocated in a new memory location and then constructed from the existing object in a way that must guarantee that the existing object is invalidated in some way. This must be handled explicitly in the implementation. For example, for std::vector this means that the move-constructed container steals the internal pointer to the data buffer from the old vector and resets the old vector to empty. This avoids a big and usually superfluous copy operation.
Re:You can't Rust that by masklinn on Saturday March 31, 2018 @02:59PM (#16724587)
> in a way that must guarantee that the existing object is invalidated in some way

That statement is confusing as the spec very specifically notes that the moved-from object remains valid, so saying that it's invalidated is odd.

Re:You can't Rust that by gmueckl on Saturday March 31, 2018 @03:06PM (#16724625)
You are confusing the now invalid internal state with the continuing existence of an accessible object at the old location. So you can move from an object and then access it. The standard guarantees that the object is still around. But because its data got stolen, its internal state must now be something invalid or empty depending on the semantics of the object.
Re:You can't Rust that by cornstalks on Saturday March 31, 2018 @04:21PM (#16724990)
You’re using way too strong of language. A move in C++ doesn’t require the original object to be invalidated or even modified in any way. POD-ish types, for example, are “moveable” despite not being made empty or invalid after a move (their move is implemented as a copy, which is perfectly valid).
Re:You can't Rust that by MereInterest on Saturday March 31, 2018 @04:46PM (#16725100)
As an example of this, in `boost::asio`, a moved-from socket is in a valid state, and is ready to accept a new connection.

https://www.boost.org/doc/libs/1_54_0/doc/html/boost_asio/re...

Re:You can't Rust that by masklinn on Saturday March 31, 2018 @04:55PM (#16725134)
> You are confusing the now invalid internal state with the continuing existence of an accessible object at the old location.

I'm not confusing anything, and the spec very explicitly notes that a moved-from object is in a "valid but unspecified state", not in an invalid state.

> The standard guarantees that the object is still around.

The standard's guarantees are significantly stronger than that, and furthermore

> But because its data got stolen, its internal state must now be something invalid or empty depending on the semantics of the object.

That is absolutely not a hard rule, a trivial move constructor is a copy constructor and does not affect the moved-from object in any way.

Re:You can't Rust that by wilun on Saturday March 31, 2018 @07:54PM (#16725823)
> "valid but unspecified state", not in an invalid state

That's mostly a detail because you can't rely on that to do anything interesting. Basically you can assign a new value to the object, or just destruct it. Having a "valid" but unknown value is completely useless.

What is sad is that the same mechanism has been overloaded for at least 2 different things: simple perf optim (when you fall into this situation of no guarantee), and movable unique ownership (like unique_ptr) where you actually can read the resulting object after the move an expect a predictable (in a portable and standardized way ) outcome.

Re:You can't Rust that by Sharlin on Saturday March 31, 2018 @03:00PM (#16724593)
As far as I know Rust doesn't really do C++-style copy elision currently. So things passed by value get moved. Which is to say, bitwise copied (memmove) and the moved-from binding statically marked unusable unless it implements the Copy trait, basically promising that it doesn't contain owned references to external resources (in which case copying would violate the single-owner invariant).

In C++ an object can be passed by reference, by copy (implicitly invoking the copy constructor), by move (implicitly invoking the move constructor), and additionally the compiler may choose to elide the copy/move. Whereas in Rust, you can only pass by reference (borrowing) or by bitwise copy (moving). There are no implicit copy or move constructors, and AFAIK at least currently the compiler doesn't hoist objects directly to the callee's stack frame.

Re:You can't Rust that by dbaupp on Saturday March 31, 2018 @05:46PM (#16725339)
I don't think you're quite right with the consequences of C++ style copy elision. It fundamentally isn't nearly as important as in Rust because it doesn't implicitly copy. Especially pre-C++11, copy elision was critical to ensure that functions can return std::strings and std::vectors without doing expensive copies. This doesn't apply to Rust which started with move semantics built-in, and cheap move semantics at that (even cheaper than C++ in some respects, as objects don't need to be left in a valid-but-unspecified state).

Stepping into the specifics of the options, you've classified things a little inconsistently: C++ allows arguments to be references or values, and the caller can choose for the latter to be by copy or by move. This the same as Rust, with references and values and the caller choosing by copy (with an explicit .clone()) or by move (default).

You can argue that Clone isn't part of the language, or that being explicit doesn't count, but I don't think that's so interesting (certainly by move in C++ is often equally explicit: std::move).

And, without move constructors, hoisting things into caller's stack frames is just a run-of-the-mill optimisations for returned values following the standard "as-if" rule, no need for explicit enabling in the standard. C++ needs it because part of the semantic model is running user-defined copy/move constructors on 'return', so the standard needs to make it okay to not do this in some cases, but without the user-defined code, it isn't necessary.

Re:You can't Rust that by Sharlin on Saturday March 31, 2018 @06:09PM (#16725424)
Yes, Rust move semantics are designed to make passing things like strings and collections cheap. Only the stack-allocated part must be copied. But passing stack-heavy objects (think std::array) can still be expensive without elision guarantees, so some elision rules could be useful even though they don’t affect semantics and would be allowed anyway by the as-if rule. People shouldn’t feel the need to pass by reference just for performance reasons, especially given that it introduces syntactic noise at call site unlike in C++. OTOH I guess inlining makes the point moot in many cases.
Re:You can't Rust that by pcwalton on Saturday March 31, 2018 @03:27PM (#16724728)
> AFAIK at least currently the compiler doesn't hoist objects directly to the callee's stack frame.

Yes, it does. This is the "retptr" optimization.

Re:You can't Rust that by oconnor663 on Saturday March 31, 2018 @04:12PM (#16724948)
Doesn't the LLVM backend make tons of similar optimizations under the hood?
Re:You can't Rust that by pcwalton on Saturday March 31, 2018 @04:28PM (#16725020)
Yes, especially if callees are inlined.
Re:You can't Rust that by nurettin on Sunday April 01, 2018 @05:58AM (#16727547)
At some point I stopped worrying and started passing by value. Compiler would sometimes optimize extra copies, the big objects that got copied would show up during profile. Works better than chasing move bugs if you are just writing ERP software and don't really care about cache misses. Edit: c++
Re:You can't Rust that by phkahler on Saturday March 31, 2018 @07:19PM (#16725693)
If pointers are useless, how do you create complex data structures. In a C++ program I have a struct that is nothing but 5 pointers (4 now since 2 can be stored as their XOR). I'm starting to wonder about this Rust thing that's been sounding so awesome...
Re:You can't Rust that by gpm on Saturday March 31, 2018 @08:31PM (#16725980)
Pointers aren't useless... things don't move if you don't tell them to. The compiler can statically guarantee they don't move while you have a pointer to them (via references/lifetimes) or with unsafe code you can just tell the compiler that "I checked and I don't move this thing". I've had plenty of rust datastructures that are just a collection of pointers.

The distinction I guess is that in rust you can move things assuming you have no references to them, while in c++ you can't really (I mean, move constructors, but that's really a fancy copy where the initial thing still exists in some form). And moving things is pretty normal - as is taking pointers to them - just not at the same time.

Re:You can't Rust that by jay-anderson on Sunday April 01, 2018 @02:18AM (#16727019)
> things don't move if you don't tell them to

This is something I've struggled a bit with when playing with Rust. I've had a hard time understanding whether I'm telling the compiler to move or copy. In other words I need to understand better what I'm telling it to do (I probably just need to buckle down and study the new book in this area better). It wasn't immediately obvious to me when I last played with it.

Re:You can't Rust that by masklinn on Sunday April 01, 2018 @07:27AM (#16727748)
> I've had a hard time understanding whether I'm telling the compiler to move or copy. In other words I need to understand better what I'm telling it to do

You're not telling it anything, in Rust copy v move is a type-level concern: either the type is "move" (the default) or it's "copy".

There is no difference at the machine-level either (it's semantically doing a memcpy either way, Rust has no move ctor v copy ctor and its move semantics are somewhat different than C++), the difference is whether the compiler will let you use the source object afterwards.

Incidentally, I believe you're not really "telling the compiler to move or copy" in C++ either, you're only suggesting that it can move and whether it actually does so depends on the type's ctors and IIRC the receiver's overloads: std::move on a type which can't be moved or with a target which doesn't care does not do anything.

Re:You can't Rust that by steveklabnik on Sunday April 01, 2018 @02:53AM (#16727110)
It depends on the type, not what you say. Copy types copy, other types move.
Re:You can't Rust that by smaddox on Sunday April 01, 2018 @10:56AM (#16728587)
Box (an owned pointer to heap-allocated struct), Rc (reference-counted heap-allocated shared struct), and Arc (atomicly reference-counted heap-allocated shared struct) cover any cases that indicies into an array (or other data structure) don't.
Re:You can't Rust that by pcwalton on Saturday March 31, 2018 @08:05PM (#16725862)
Unique pointers or reference counted pointers.