13

Looking through this article on Rust's concurrency safety:

http://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html

I was wondering how many of these ideas can be achieved in C++11 (or newer). In particular can I create an owner class that transfers ownership to any method to which it may be passed? It seems that C++ has so many ways to pass variables that it would be impossible, but maybe I could put some restrictions on the class or template to ensure that some template code gets executed with every method pass?

  • Some quotes from the link would improve this question – Martin Ba May 10 '16 at 17:57
  • 2
    @delnan (Safe) Rust guarantees that you never have more than one mutable reference to something at a time and that you never have have a mutable reference to a thing you're also having read-only references to. It also has some restrictions on transferring data between threads. Together these prevent a significant class of threading related bugs and make reasoning about the state of objects easier, even in single threaded code. – CodesInChaos May 10 '16 at 20:37
  • 3
    You don't think you can express borrowing in a way that the C++ compiler could verify, so you'd have to resort to runtime enforcement with the associated performance hit. – CodesInChaos May 10 '16 at 20:38
  • 1
    Aren't scope ownerships already implemented by smart pointers in C++11? – Akshat Mahajan May 14 '16 at 2:53
  • 1
    @JerryJeremiah Rust has a wide variety of reference types. The basic ones, &, do not require any sort of promoting to be used. If you try to get a &mut while you've still got another reference (mutable or not) to the same item, you won't be able to compile. RefCell<T> moves the check to run time, so you'll get a panic if you try to .borrow_mut() something that already has an active .borrow() or .borrow_mut(). Rust also has Rc<T> (shared owning pointer) and its sibling Weak<T>, but those are about ownership, not mutability. Stick a RefCell<T> inside them for mutability. – 8bittree Nov 17 '17 at 20:19
6

C++ has three ways to pass parameters to a function: by value, by lvalue reference, and by rvalue reference. Of these, passing by value creates ownership in the sense that the called function receives its own copy, and passing by rvalue reference indicates that the value may be consumed, i.e. will no longer be used by the caller. Passing by lvalue reference means that the object is temporarily borrowed from the caller.

However, these tend to be “by convention” and cannot always be checked by the compiler. And you can accidentally turn a lvalue reference into an rvalue reference using std::move(). Concretely, there are three problems:

  • A reference can outlive the object it references. Rust's lifetime system prevents this.

  • There can be more than one mutable/non-const reference active at any time. Rust's borrow checker prevents this.

  • You cannot opt out of references. You cannot see at a call site whether that function creates a reference to your object, without knowing the signature of the called function. Therefore you cannot reliably prevent references, neither by deleting any special methods of your classes nor by auditing the call site for compliance with some “no references” style guide.

The lifetime problem is about basic memory safety. It is of course illegal to use a reference when the referenced object has expired. But it is very easy to forget about the lifetime when you store a reference within an object, in particular when that object outlives the current scope. The C++ type system cannot account for this because it doesn't model object lifetimes at all.

The std::weak_ptr smart pointer does encode ownership semantics similar to a plain reference, but requires that the referenced object is managed via a shared_ptr, i.e. is reference-counted. This is not a zero-cost abstraction.

While C++ has a const system, this doesn't track whether an object can be modified, but tracks whether an object can be modified through that particular reference. That does not provide sufficient guarantees for “fearless concurrency”. In contrast, Rust guarantees that if there is an active mutable reference that is the only reference (“I am the only one who can change this object”) and if there are non-mutable references then all references to the object are non-mutable (“while I can read from the object, no one can change it”).

In C++ you might be tempted to guard access to an object through a smart pointer with a mutex. But as discussed above once we have a reference it can escape its expected lifetime. Therefore such a smart pointer cannot guarantee that it is the single point of access to its managed object. Such a scheme may actually work in practice because most programmers don't want to sabotage themselves, but from a type-system view point this is still completely unsound.

The general problem with smart pointers is that they are libraries on top of the core language. The set of core language features enables these smart pointers, e.g. std::unique_ptr needs move-constructors. But they cannot fix deficiencies within the core language. The abilities to implicitly create references when calling a function and to have dangling references together mean that the core C++ language is unsound. The inability to limit mutable references to a single one means that C++ cannot guarantee safety against race conditions with any kind of concurrency.

Of course in many respects C++ and Rust are more alike than they are disalike, in particular regarding their concepts of statically determined object lifetimes. But while it is possible to write correct C++ programs (provided none of the programmers makes any mistakes), Rust guarantees correctness regarding the discussed properties.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.