5 ms·
Emulating the Rust borrow checker with C++ move-only types
- pcwalton 9y agoThe Rust borrow checker checks borrows: that is, references. This doesn't emulate borrows, but rather emulates Rust-style moves. An attempt at a more accurate statement would be that this emulates the use-after-move checking that the Rust compiler does. The problem is that that it doesn't do that either: it statically prevents copies but doesn't prevent use-after-move. I think the accurate way to describe this pattern would be that it disallows copies and forces you to annotate moves with the move keyword. This is somewhat similar to what Rust does, in that non-Copy types are moved by default. The difference is that you don't have to write "std::move" in Rust: the compiler just infers the right thing to do. It's a little hard to map this onto Rust semantics to begin with, since fundamentally all this is is not having a copy constructor, which is a concept that doesn't exist in Rust in the first place.
- masklinn 9y ago> The difference is that you don't have to write "std::move" in Rust: the compiler just infers the right thing to do. And of course since it's the language's default you don't have to write a dozen lines of boilerplate either, this is the C++ code stripped of the interspersed text: class MoveOnlyInt final { private: int i; void release() { /* call to release function here */ } public: explicit MoveOnlyInt(int i) : i(i) {} ~MoveOnlyInt() { release(); } MoveOnlyInt() = delete; MoveOnlyInt(const MoveOnlyInt &) = delete; MoveOnlyInt& operator=(const MoveOnlyInt &) = delete; MoveOnlyInt(MoveOnlyInt &&other) { i = other.i; other.i = -1; } MoveOnlyInt& operator=(MoveOnlyInt &&other) { release(); i = other.i; other.i = -1; return *this; } operator int() const { return i; } }; This seems like a fair Rust version, with as you noted the "move source" becoming inaccessible rather than just being in an invalid state: struct MoveOnlyInt { i: isize, } impl MoveOnlyInt { fn new(i: isize) -> MoveOnlyInt { MoveOnlyInt { i: i } } } // Rust doesn't have an "operator int", this seems close enough impl<'a> From<&'a MoveOnlyInt> for isize { fn from(m: &'a MoveOnlyInt) -> isize { m.i } }
- kibwen 9y agoThat's actually even way too much code for the Rust version. A wrapper type declaration is as simple as `struct MoveOnlyInt(i32);`, and construction is then just `let foo = MoveOnlyInt(42);`. (And of course, the wrapper type is only necessary because integers in Rust implement `Copy`, opting out of move semantics.)
- steveklabnik 9y ago(and "operator int" would be foo.0 in this case)
- mrout 9y agoIt's actually far too little code, as it leaves out all the code in the Rust compiler that implements this.
- heinrich5991 9y agoDon't use `isize`, `isize` is not equivalent to C's `int`, but `intptr_t`. Specific sizes would be best, but if you need to copy C's `int`, there's `libc::c_int`.
- Animats 9y agoRight. This isn't about borrowing; it's about taking ownership. Single ownership is half the problem. Enforcing borrow semantics means passing references/pointers you can use, but can't keep or delete. Trying to do this in C++ means either extreme restrictions on not being able to copy a reference/pointer, or the possibility of some function keeping a reference/pointer beyond the lifetime of the object. There's no way to say "it's OK to pass or copy this to a scope with a shorter lifetime". The C++ type system doesn't understand "inner scope". Trying to retrofit memory safety to C++ without breaking backwards compatibility is hard. I struggled with this years ago. It turns out it's in some ways easier to do for C than for C++.[1] [1] http://www.animats.com/papers/languages/safearraysforc27.pdf http://www.animats.com/papers/languages/safearraysforc27.pdf
- bhuztez 9y agotake a look at mine ( https://github.com/bhuztez/borrow https://github.com/bhuztez/borrow )
- throwaway91111 9y agoIsn't this just a UniquePtr? Ie it allows moves, but references to it are not guaranteed to be valid. The latter is the borrow checker.
- dbaupp 9y agounique_ptr requires an allocation whereas this does not, but it is using the same language features as unique_ptr.
- akubera 9y agoYeah, the unique_ptr<T> enforces move semantics and can be "borrowed" via get() (but unlike rust, further borrowing/moving rules are not enforced by C++). This post is an explanation of how to write your own class which enforces this for objects whether or not they are on the heap. It's a good question to ask whether implementing a move-only class is more beneficial than wrapping a normal class in a unique_ptr. This comes down to trusting the programmer to know when to copy/move/delete (for better or worse). The best policy is probably to implement ALL constructor/assignments so there's minimal frustration when, say, trying to write a subclass, and have a special subclass `UniqueFoo` that deletes copy methods to enforce Moveability - minimizing surprise and maximizing flexibility.
- youdontknowtho 9y agoAnd then someone heard the Rust mafia say "Form the MegaZord!!!"...and many corrections were posted.
- kibwen 9y agoYeah, who'da thunk that saying technically inaccurate things on a site full of pedantic nerds would meet resistance. :P
- youdontknowtho 9y agoMan! Rough crowd! Is there a 'joke' tag that I'm missing or something?
- adrianN 9y agoHN readers tend to downvote jokes.
- youdontknowtho 9y agoYes. Yes I see that now. Yet another thing I can't talk about on hacker news. Well. Ok, then.
- dikaiosune 9y agoNot funny ones, in my experience
- petters 9y agoAs others have pointed out, this does not really emulate the Rust borrow checker. You can still have multiple references and pointers to the same object.
- 0xFFC 9y agoBoy I am gonna get down voted, But I rather say this. I do consider myself C++ programmer, and I like it, but truth is truth. I think C++ designers are very well respected scientist. But they are not programmers. This is sad truth. Every version of C++ comes out they tend to make C++ more complicate and introduce more ways to do something which was possible before. I was/am very happy with overall design of modern C++. But they did a very bad strategic mistake. Instead of making language simpler or at improving what was there, they are introducing new syntax for everything. Let me give you instance. Imagine we have metafunction is_void. Before C++11, there was only one way to call it : > is_void<t>::value C++11 they added : > bool(is_void<t> {}) C++14 they added : >is_void<T>{}() and there was plan to add this syntax to C++17: >is_void_v<T> Okay. I am not scientist. And I do know someone people in committee are well respected scientist and tbh I like their job alot. But when it comes practical stuff. This is nuts. Do they think how much makes this reading code bases hard ? How much makes tooling hard ? How much makes syntax unapproachable ? I don't understand rational behind this, Why would they add new syntax for what already possible, when they can improve core language/libraries more and more.