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.
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.
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.
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;
};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.
This is not actually true, but it's close enough for your purposes here.
But just to be clear about it, see stuff like this: https://stackoverflow.com/questions/58339165/why-can-a-t-be-...
> Because they lack a statically known size, these types can only exist behind a pointer. Any pointer to a DST consequently becomes a wide pointer consisting of the pointer and the information that "completes" them (more on this below).
That's for slices, for dynamically sized types (eg. `Box<dyn ToString>`) it contains a pointer to the virtual table.
> Rust `Arc` = C++ `std::shared_ptr`
GP says:
> Rust requires shared pointers (Arc) to also explicitly implement some sort of Mutex-equivalent runtime safety check in order to mutate the data.
Which is it?
> An example of a big C++ codebase using something similar is Chromium ...
Chromium's smart pointers are similar to their standard counterparts -- no mutexes for write access to pointed data.
Also, tangent but interesting: From https://www.chromium.org/developers/smart-pointer-guidelines...:
> Reference-counted objects make it difficult to understand ownership and destruction order, especially when multiple threads are involved. There is almost always another way to design your object hierarchy to avoid refcounting
I mentioned Chromium because they also differentiate between thread safe and non-thread safe shared pointers.
If anything, Rust shared pointers are more similar to C++ std pointers because in Chromium the reference count is inside the class, which is very handy because you can reconstruct a smart pointer from a raw pointer (like `this`), at the cost of needing `T` to extend `base::RefCounted`.
- RefCounted: It's like shared_ptr but refcount load/modify/store operation is not atomic, thus not thread-safe. No synchronization for pointed data.
- RefCountedThreadSafe: It's like shared_ptr. This means refcount load/modify/store is atomic, so has overhead, yet safe to pass across thread boundaries. Again, just like shared_ptr, no synchronization for pointed data.
- Locker class above: It's an (incomplete) wrapper around shared_ptr where read-only access goes through a shared lock and rw access goes through an exclusive lock. I suppose this is what rust's ARC guarantees at compile-time with less overhead the sketch above?
So;
> Both are true, Rust just has more restrictions.
No, both are not true, my understanding of ARC ~= Locker && ARC > shared_ptr
Your `Locker` does not do what `Arc` does, even at compile time, because it does not allow concurrent access, like an `Arc<AtomicBool>` would. Your `Locker` is more like an `Arc<RwLock<T>>`.
Best equivalent you can get in C++ is `Arc<T>` = `std::shared_ptr<const T>`.
https://doc.rust-lang.org/std/sync/struct.Arc.html
> Shared references in Rust disallow mutation by default, and Arc is no exception: you cannot generally obtain a mutable reference to something inside an Arc. If you need to mutate through an Arc, use Mutex, RwLock, or one of the Atomic types.
I guess you could get the final pieces to get something similar by creating `Send` and `Sync` traits in C++: https://doc.rust-lang.org/nomicon/send-and-sync.html. I think the main pain point here is that you cannot auto-derive `Send` and `Sync` so it would end up being very verbose.
So basically Rust is combining object ownership and thread safety while C++ keeps thread safety separate, which would seem to provide more flexibility, but also lets you shoot yourself in the foot.
Just thinking out loud, I wonder if C++ could better address this by also having a class of thread-aware smart pointers? -- but the problem is that C++ always has the old/new (C, C++) way of doing things - pthreads vs std::thread, std::mutex, etc, so even if the language provides easier ways of writing bug free code, there is no way to force developers to use those facilities.
In C++ there is also the issue of how to make statically allocated data structures thread safe in an enforceable way. Another kind of smart reference object, perhaps? Disallow global objects not accessed by such references?
C++ (which I have used since long before C++11) really wants to be two conflicting things - encompassing C's low level role as the ultimate systems programming language with no guardrails, while also wanting to compete as a much higher-level safer language for application developers. Perhaps the two safe+unsafe roles can be better combined into one language if one were to start from scratch. I'm not sure that Rust gets it right either - erring in the other direction by not being flexible enough.
I ask because I can think of a few ways it’s less flexible than C, but I also think that effect is massively overstated by people who aren’t familiar with the language. There are OS kernels written in Rust, for example.
The rust borrow checker works on values, and all that, not just on objects with RAII.
This will not compile:
let x = 42;
let r1 = &x;
let m1 = &mut x;
println!("{r1}");It will help you understand why "smart pointers" still won't help you.
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.
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.
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?
This is somewhat typical of where C++ is at nowadays - layering new functionality on top of old that wasn't designed to accommodate it. In an ideal world the language and libraries would be refactored and rationalized, but of course backwards compatibility precludes that. This is the fate of old languages - stay unchanged and become obsolete, or keep layering on new functionality and become messy and inconsistent.
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!
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.
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.
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.
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.
If you really believe that Google and FaceBook (etc, etc) hire morons who don't care if their code works, then you are not qualified to talk about programming languages.
But Rust now has a large amount of mindshare there and is being used a lot in new projects.
T t1; // stack, reference as t1
T* t2 = new T(); // heap, raw pointer, reference as * t2
std::unique_ptr<T> t3 = std::make_unique<T>(); // heap, smart pointer, reference as * t3
T* pt = &t1; // Create a raw pointer to t1! Bad idea!
Why not? What if you have some function f(T *) that you want to call?
But anyway, we're not _just_ talking about stack allocations, but also extra levels of indirection on the heap. For example, vectors store their elements in a heap-allocated buffer directly. If they kept them all in shared pointers, there would be an extra level of indirection. This means e.g. vector::operator[] has to return a reference (which is basically the same thing as a pointer under the hood); it can't return shared_ptr or similar (because storing all its elements as shared pointers would make it way slower due to the extra allocations).
In Rust, vector access is safe (due to the borrow checker), but in C++, it's not.
vector<int> v {1, 2, 3};
int& x = v[0];
v.push_back(4);
printf("%d\n");
This code is UB in C++. In Rust, it's impossible to write something like this. fn main() {
let mut v = vec![1, 2, 3];
let x = &v[0];
v.push(4);
println!("{x}");
}
This code fails to compile.In C++ (vs C), if the intent is to pass something large efficiently, then you'd use a reference parameter, not a pointer.
You seem to be confused about the meaning of C++ smart pointers - the whole point of them (as a replacement for C's raw pointers) is that they control and indicate ownership. You can't just assign a smart pointer to something you don't own (like an element of a vector). You can copy a shared_ptr to create an additional reference, or move a unique_ptr to move ownership.
A C++ compiler might generate a warning for that invalidated reference. clang++ is generally much better than g++, but I agree it'd be nice if a conforming compiler was forced to at least flag it, if not reject it.
The problem with doing this in the general case, where it's a user-defined (or library defined, as here) data structure, rather than one defined by the language, is that the compiler needs to inspect the implementation of that "push" method and realize that it might do something to invalidate references (& iterators). In the case of a library the compiler won't have access to the implementation to figure that out. How would Rust handle this if "vec" were a user-defined type where only the definition (not implementation) was available - how would it know that the push() was unsafe?
Sure, sorry, I was using "pointer" and "reference" interchangeably. Indeed, references are pointers under the hood.
> You seem to be confused about the meaning of C++ smart pointers
I am not confused at all. I understand exactly what unique_ptr and shared_ptr are in C++. They are basically the equivalent of Rust's Box and Arc (except that they can be null), but I used C++ before Rust so I learned about unique_ptr and shared_ptr first.
You are the one who asked what the advantage of Rust's borrow-checker is over C++-style memory management with smart pointers, but you seem to understand that it doesn't make sense to use smart pointers everywhere. Aren't you answering your own question? The advantage of Rust over C++ is that the borrow checker helps you in the cases where it doesn't make sense to use smart pointers / heap allocations.
You are the one who is maybe confused about what the borrow checker even is/does.
> A C++ compiler might generate a warning for that invalidated reference.
Neither clang nor g++ does so, even with -Wall. I just checked. How could they?
> I agree it'd be nice if a conforming compiler was forced to at least flag it, if not reject it.
If you did this then you would have basically reinvented the borrow checker.
> The problem with doing this in the general case, where it's a user-defined (or library defined, as here) data structure, rather than one defined by the language, is that the compiler needs to inspect the implementation of that "push" method and realize that it might do something to invalidate references (& iterators).
Not in Rust. It only needs to inspect the declaration. That is the whole point of the borrow checker. The fact that you think this can only be done for built-in types is what made me suspect that you don't understand what the borrow checker is.
The declaration of the indexing operator for Vec<T> is roughly (getting rid of some irrelevant details):
fn index(&self, i: usize) -> &T
This is shorthand for fn index<'a>(&'a self, i: usize) -> &'a T
Those references (the `&self` and the returned `&T`) have the same lifetime. That lifetime cannot overlap with any lifetime of a _mutable_ reference to the same data. `push` can be declared like so: fn push(&mut self, value: T)
Because this requires a mutable reference to `self`, the compiler statically checks that it does not overlap with any other reference to the same data, which includes the reference returned by the indexing operation, which is why the example I gave won't compile. This works the same way with user-defined types; Vec is not special in any way.The reason you can't do a similar thing in C++ is because it has no syntax for lifetimes. If you had a function on vector like
const T& index(size_t i)
you have no idea if the returned `T` is derived from `this` or from somewhere else, so you don't know what its lifetime should be.How exactly is this defined for something like index() which is returning a reference to a different type than the object itself, and where the declaration doesn't indicate that the referred to T is actually part of the parent object? Does the language just define that all references (of any type) returned by member functions are "invalidated" (i.e. caught by compiler borrow checker) by the mutable member call?
What happens in Rust if you attempt to use a reference to an object after the object lifetime has ended? Will that get caught at compile time too, and if so at what point (when attempt is made to use the reference, or at end of object lifetime) ?
Yes, exactly.
> How exactly is this defined for something like index() which is returning a reference to a different type than the object itself, and where the declaration doesn't indicate that the referred to T is actually part of the parent object?
Only if they have the same lifetime (the 'a in my example). For example, imagine a function that gets an element of a vector and uses that to index into another vector. You might write it like this:
fn indirect_index<'a, 'b, T>(v1: &'a Vec<usize>, v2: &'b Vec<T>, i: usize) -> &'b T {
let j = v1[i];
&v2[j]
}
The returned value is not invalidated by any future mutations of the first vector, but only the second vector, since they share the lifetime parameter 'b.> What happens in Rust if you attempt to use a reference to an object after the object lifetime has ended?
This is prevented at compile time by the borrow checker. E.g.:
// this takes ownership of the vec,
// and just lets it go out of scope
fn drop_vec<T>(_v: Vec<T>) {
}
fn main() {
let v = vec![1, 2, 3];
let x = &v[0];
drop_vec(v);
println!("{x}");
}
This program fails to compile with the following error: error[E0505]: cannot move out of `v` because it is borrowed
--> src/main.rs:9:14
|
7 | let v = vec![1, 2, 3];
| - binding `v` declared here
8 | let x = &v[0];
| - borrow of `v` occurs here
9 | drop_vec(v);
| ^ move out of `v` occurs here
10 | println!("{x}");
| --- borrow later used hereJust by having built-in knowledge of standard library types such as std::vector, the same way the compiler has built-in knowledge of some library functions such as C's printf().
I wouldn't expect such policing to be perfect, but the compiler could at least catch simple cases where reference/iterator use follows an invalidating operation in the same function.
Don't get me wrong - I'm not defending C++. It's a beast of a language, and takes a lot of experience and self-discipline to use without creating bugs that are hard to find.
Right, but you were asking what advantage Rust has over C++, which is what I'm trying to explain. (If you had instead asked what advantage C++ has over Rust, I'd have given a very different answer!)
> It's a beast of a language, and takes a lot of experience and self-discipline to use without creating bugs that are hard to find.
Rust makes creating a certain class of these hard-to-find bugs much harder.
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).