4 ms·
The main overhead of using shared/unique ptr for everything where you could have used stack allocation is not the extra method call for get etc, it’s the extra
by School-Cotton 3y ago
The main overhead of using shared/unique ptr for everything where you could have used stack allocation is not the extra method call for get etc, it’s the extra heap allocation. Compilers can probably inline get, but they can’t change heap allocations to stack allocations in general.
- HarHarVeryFunny 3y agoIf you're declaring an object on the stack, then there is no reason to be using a pointer to refer to it. You could take the address of it and assign that to a raw pointer if you wanted to for some (perverse!) reason, but you'd never then assign that to a shared/unique_ptr since that implies ownership. 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!
- School-Cotton 3y ago> If you're declaring an object on the stack, then there is no reason to be using a pointer to refer to it. 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.
- HarHarVeryFunny 3y ago> Why not? What if you have some function f(T *) that you want to call? 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?
- School-Cotton 3y ago> In C++ (vs C), if the intent is to pass something large efficiently, then you'd use a reference parameter, not a pointer. 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.