19

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?

jaggi
  • 103
  • 3
Brannon
  • 361
  • 1
  • 9
  • Some quotes from the link would improve this question – Martin Ba May 10 '16 at 17:57
  • What does "transfer ownership to any method to which it may be passed" mean? C++ has move constructors, they're just not the default. Please elaborate. –  May 10 '16 at 19:58
  • 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
  • @CodesInChaos I know Rust, and I agree that the things you mention seem impossible to do (at compile time) with C++. But I would like to hear from OP which things they want to do in C++, to give a targeted assessment of whether that is possible. –  May 11 '16 at 06:10
  • You could use the same concept as a recursive mutex uses which is "which thread locked me to begin with?". So you would create a "threadsafe guard accessor" of sorts for whatever it is you're passing around that would only allow the thread who "owns" the object to change it. IMHO It's probably more trouble than it's worth.; A cleaner design that facilitates understanding, debug, and fault injection adds more value than "fearless concurrency". – ppetraki May 11 '16 at 14:37
  • @delnan, I was specifically interested in the item CodesInChaos summarized: having the compiler ensure that an item is not mutable in multiple threads simultaneously. – Brannon May 11 '16 at 18:44
  • 1
    Aren't scope ownerships already implemented by smart pointers in C++11? – Akshat Mahajan May 14 '16 at 02:53
  • I don't know Rust but it seems to me that the mutable and readonly references are very similar to owning and weak pointers. Both mutable and readonly can be weak pointers. You only allow one mutable owning pointer at a time and the weak pointers need to be upgraded to owning pointers before they can before used. It has to be done at runtime. But what happens in Rust when you want a mutable reference to something that already has multiple readonly references? – Jerry Jeremiah Oct 15 '16 at 20:17
  • 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` 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` (shared owning pointer) and its sibling `Weak`, but those are about ownership, not mutability. Stick a `RefCell` inside them for mutability. – 8bittree Nov 17 '17 at 20:19

1 Answers1

12

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.

amon
  • 132,749
  • 27
  • 279
  • 375
  • If the issue is that C++ doesn't track ownership in the core language, would it be possible to implement that functionality through meta-programming? Meaning that you would create a new smart pointer class which would be memory safe by (1) forcing it to point exclusively to objects which only use smart pointers from the same class and (2) tracking ownership through templates – Elliot Gorokhovsky Aug 31 '19 at 19:05
  • 4
    @ElliotGorokhovsky No, because the template cannot disable core language features such as references. A smart pointer can make it more difficult to get a reference, but at that point you're fighting the language – most standard library functions need references. It is also not possible to check the lifetime of a reference through templates because the language offers no reified concept of lifetime. – amon Aug 31 '19 at 20:46