Hacker News new | comments | show | ask | jobs | submit login
Emulating the Rust borrow checker with C++ move-only types (nibblestew.blogspot.com)
96 points by buovjaga on May 13, 2017 | hide | past | web | favorite | 20 comments



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


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


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


That'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.)


(and "operator int" would be foo.0 in this case)


Don'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`.


take a look at mine ( https://github.com/bhuztez/borrow )


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


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


unique_ptr requires an allocation whereas this does not, but it is using the same language features as unique_ptr.


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


Deprecating old and arguably inferior syntax is part of the evolution of a language.

C++ is not going anywhere, so improvements are welcome. Upgrading from is_void to is_void_v can be done automatically.


FYI they didn't deprecate anything, they just augment new syntax.


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


And then someone heard the Rust mafia say "Form the MegaZord!!!"...and many corrections were posted.


Yeah, who'da thunk that saying technically inaccurate things on a site full of pedantic nerds would meet resistance. :P


Man! Rough crowd! Is there a 'joke' tag that I'm missing or something?


HN readers tend to downvote jokes.


Not funny ones, in my experience


Yes. Yes I see that now. Yet another thing I can't talk about on hacker news.

Well. Ok, then.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: