41 ms·
Can someone familiar with both please explain the benefit of Rust's borrow checker memory management model over C++'s std::unique_ptr and shared_ptr ? Is there
by HarHarVeryFunny 3y ago
Can someone familiar with both please explain the benefit of Rust's borrow checker memory management model over C++'s std::unique_ptr and shared_ptr ? Is there some safety argument to prefer Rust's model, or is it something else ?
I'm not aware of any C++ compiler doing it, but it seems smart pointer overhead could be automatically and safely reduced (in same way one can do it manually) by the compiler lowering the generated code to use raw pointers where permissible.
- lifthrasiir 3y ago> it seems smart pointer overhead could be automatically and safely reduced (in same way one can do it manually) by the compiler lowering the generated code to use raw pointers where permissible. The sheer difficulty of doing this is one of the motivations behind Rust's borrow checker, which uses a combination of type system and static analyses to prove the safety without running anything. In fact this problem is probably easier to solve for languages where everything is GC-managed; those languages would have a heavy runtime which can transparently handle that in principle!
- School-Cotton 3y agoRust has unique and shared pointers too (Box and Arc/Rc). But using them when unnecessary results in extra heap allocations. I’m not aware of C++ compiler that can consistently rewrite uses of unique_ptr to heap-allocated objects to use raw pointers to stack-allocated objects instead.
- lionkor 3y agoTheres nothing special about unique_ptr, if you dont want allocations and youre ok with just moving your values around directly, you use value and move semantics.
- School-Cotton 3y agoMove and value (deep copy) semantics exist in Rust too, but neither of those does the same thing as passing a raw pointer (or reference). Which you can do in c++, but not safely. That’s the difference with Rust. In C or C++ if a function/method takes a raw pointer (or some other lifetime-constrained type like string_view), I have no idea if it’s going to stash it somewhere and try to look at it again later. If it returns a raw pointer or reference, I don’t know whether it is going to get invalidated by some future call. Iterator invalidation is a huge source of UB in C++ but completely unknown in rust. Clearly having a hash map where all the values are stored indirectly in shared_ptr would let you provide a safe access API, but would be horrible for performance. In Rust you can have the safe API without compromising on efficiency.
- HarHarVeryFunny 3y agoI didn't mean trying to rewrite code to change dynamically allocated objects to stack based ones. That sounds more like an optimization that a managed language like C# might do. C++'s unique_ptr and shared_ptr both have a get() method that will return you a raw pointer to the managed object, which can be a safe optimization within a function holding ownership to the object, as well as allowing you to use legacy functions on it that take raw pointer arguments. I was thinking the C++ compiler could itself realize when it is safe to do so, save the raw pointer to a temp variable, and "rewrite" smart pointer accesses to use this temporary raw pointer. One could even imagine the compiler changing smart pointer function parameters to raw pointers in some circumstances.
- School-Cotton 3y agoThen I’m afraid I don’t know what your point is. Rust’s borrow-checker isn’t a replacement for shared/unique pointers in C++. It’s a replacement for raw pointers.
- HarHarVeryFunny 3y agoMy point was that overhead is one common objection to C++'s shared/unique pointers - everything is a method call - but that could be mitigated by the compiler itself doing the type of raw-pointer lowering, when safe, that the get() method permits. From other replies in this thread is seems that Rust's borrow-checker addresses the high level issues of object ownership and thread safety - it's not just a replacement for raw pointers (i.e. a smart pointer), which is exactly what C++' shared/unique pointers are.
- tialaramex 3y agoborrowck is a semantic check. So, it's not a replacement for some particular C++ feature per se, it's not a feature in the sense you mean at all, it's just that while C++ and Rust both have these same semantic rules in place, Rust checks them and C++ does not. When you as a programmer inevitably get something wrong and break the rules, in Rust your program won't compile, in C++ it just has some arbitrary misbehaviour, maybe you notice, maybe you don't, maybe it matters, maybe it seems benign... until 8:26 tomorrow morning when suddenly it blows up and makes your customer very angry. In C++ the result of breaking semantic rules (not just those checked by the borrowck, most of the semantic rules in the language) is IFNDR - your program is Ill Formed, No Diagnostic Required - your entire program has no particular meaning, there is no explanation for what it does, shrug. In Rust it doesn't compile. For people whose overriding mission is to get the code to compile, C++ is very attractive. Broken garbage? Meaningless nonsense? Not my problem it compiled so I went home. If you want to write software that works, that seems like you didn't do the hard part.
- helloooooooo 3y agoRead this: https://alexgaynor.net/2019/apr/21/modern-c++-wont-save-us/ https://alexgaynor.net/2019/apr/21/modern-c++-wont-save-us/ It will help you understand why "smart pointers" still won't help you.
- HarHarVeryFunny 3y agoI read that more as a valid criticism of other parts of C++ rather than about smart pointers as a way to track ownership. e.g. std::string_view seems broken by design in wanting to support both raw-pointer based strings with zero ownership semantics as well as std::string. A string view (abstract concept) really needs to either have shared ownership of the underlying string, or have a non-owning reference that knows when it has been invalidated.
- School-Cotton 3y agoWell the string view type you wish existed seems to be exactly what Rust gives you, no? Non-owning references that "know when they have been invalidated" (or rather, the compiler prevents you from using them after they have been invalidated). I'm not sure why this means you shouldn't be able to create a string_view on top of std::string, though. You can create a Rust &str on top of String, it just doesn't participate in ownership.
- HarHarVeryFunny 3y agoMy comment was just a reply to the parent - that the linked article wasn't really about smart pointers. I was just using string_view as an example. There are lot's of places where C++'s long history shows it's ragged edges - where newer features really don't play so nice with older ones. One would certainly hope that a new language like Rust is at least initially more consistent.. the question is what will it look like in 20 years time, if it's still being actively developed at that time?
- School-Cotton 3y ago
- lionkor 3y agoThe point is that it keeps track of multiple references and disallows mutable and immutable references at the same time across threads, for example, and disallows multiple mutable references altogether. The rust borrow checker works on values, and all that, not just on objects with RAII.
- School-Cotton 3y agoMutable references can never coexist with other references in Rust, regardless of whether they're on different threads. This will not compile: let x = 42; let r1 = &x; let m1 = &mut x; println!("{r1}");
- fulafel 3y agoGenerally in C++ this kind of data transformation faces a lot of barriers. For example, the language semantics require struct fields to have the unoptimized memory layout and contents as far as the user can observe at the byte level. Low-level programming would be quite a different scene if there were a lot of permitted data optimizations by compilers (profile guided more concise representations of structs, replacing pointer based data structures with indexed layouts, etc).
- mrtracy 3y agoThe C++ smart pointers dont prevent multiple threads from mutating the pointed-to data at the same time; multiple threads can both access a unique_ptr at the same time and mutate its contents. Rust requires shared pointers (Arc) to also explicitly implement some sort of Mutex-equivalent runtime safety check in order to mutate the data. Rust also has explicit notion of thread ownership, and whether individual types are safe to pass to different threads; if a construct is not thread safe, Rust will prevent you from using it in multiple threads. As a benefit of the thread-safety notion, Rust can have two reference-counting pointer types: Arc, which uses atomic reference counting and is roughly equivalent to std::shared_ptr, and Rc which does not use atomics. Rc cannot be used across multiple threads at the same time, and the borrow checker will prevent you from doing this. Rc is appropriate for data structures which internally benefit from multiple pointers (e.g. graphs) but where all of that information is internal to a single data structure - this becomes available without paying the price of atomics.
- plq 3y agoSo ARC is something like the following? template <typename T> struct Locker { using M = std::shared_mutex; struct Locked { Locked(mtx, value) : m_lock(mtx), m_value(value) {} // operator->, operator*, get, etc. private: std::lock_guard<M> m_lock; std::shared_ptr<T> m_value; }; struct Shared { Shared(mtx, value) : m_lock(mtx), m_value(value) {} // operator->, operator*, get, etc. private: std::shared_lock<M> m_lock; std::shared_ptr<const T> m_value; }; Shared shared() { return Shared{m_mutex, m_value}; } Locked locked() { return Locked{m_mutex, m_value}; } // a nice forwarding ctor that prevents null m_value private: std::shared_ptr<T> m_value; M m_mutex; };
- fathyb 3y agoRust `Box` = C++ `std::unique_ptr`, both have the same ABI (just pointers) Rust `Arc` = C++ `std::shared_ptr` Rust `Rc` = C++ `std::shared_ptr` but using a simple integer instead of an atomic so it is not thread safe `Arc` and `Rc` do not allow you to mutate their contents directly so instead you should use "interior mutability" using something like a `Mutex` (thread-safe) or `RefCell` (not thread-safe), which have runtime checks to ensure no undefined behaviour is introduced. So `Arc<Mutex<T>>` makes it possible to mutate `T`, but `Arc<T>` cannot. Some types like atomics do not require mutability at all, so an `Arc<AtomicBool>` can be mutated directly. An example of a big C++ codebase using something similar is Chromium, where `std::shared_ptr` is forbidden and `base::RefCounted` (Rust `Rc`) and `base::RefCountedThreadSafe` (Rust `Arc`) should be used instead. WebKit does this too.