Earlier quoted context omitted.
> both have the same ABI (just pointers) 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-...
Another reason it is not true: Rust has fat pointers, eg. `std::unique_ptr ` and `Box ` both contain the same allocation data, but `Box` will be 128-bit on 64-bit systems.
Rusty.hpp: A Borrow Checker and Memory Ownership System for C++20
141–150 of 174 posts
Re: Rusty.hpp: A Borrow Checker and Memory Ownership System for C++20
#142Earlier quoted context omitted.
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.
If 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 t3 = std::m…
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 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.Re: Rusty.hpp: A Borrow Checker and Memory Ownership System for C++20
#143Earlier quoted context omitted.
Another reason it is not true: Rust has fat pointers, eg. `std::unique_ptr ` and `Box ` both contain the same allocation data, but `Box` will be 128-bit on 64-bit systems.
Where can I find details like this about Rust?
> 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).
Re: Rusty.hpp: A Borrow Checker and Memory Ownership System for C++20
#144Earlier quoted context omitted.
borrowck 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…
That last paragraph destroys your whole argument. 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.
Re: Rusty.hpp: A Borrow Checker and Memory Ownership System for C++20
#145Earlier quoted context omitted.
> both have the same ABI (just pointers) 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-...
Another reason it is not true: Rust has fat pointers, eg. `std::unique_ptr ` and `Box ` both contain the same allocation data, but `Box` will be 128-bit on 64-bit systems.
Re: Rusty.hpp: A Borrow Checker and Memory Ownership System for C++20
#146Earlier quoted context omitted.
Read this: https://alexgaynor.net/2019/apr/21/modern-c++-wont-save-us/ It will help you understand why "smart pointers" still won't help you.
I 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…
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.
Re: Rusty.hpp: A Borrow Checker and Memory Ownership System for C++20
#147Earlier quoted context omitted.
Another reason it is not true: Rust has fat pointers, eg. `std::unique_ptr ` and `Box ` both contain the same allocation data, but `Box` will be 128-bit on 64-bit systems.
What's the utility of having a 128-bit pointer on a 64-bit system ?
That's for slices, for dynamically sized types (eg. `Box`) it contains a pointer to the virtual table.
Re: Rusty.hpp: A Borrow Checker and Memory Ownership System for C++20
#148Earlier quoted context omitted.
You can actually implement the C++ behavior, if you want: unsafe fn super_unwrap (x: Option ) -> T { match x { Some(val) => val, None => unreachable_unchecked!(), } } But defaults matter, and Rust certainly doesn’t make this kind of thing ergonomic (which is a correct decision on the Rust designers’ part).
You don't have to write this, it already exists as the (unsafe of course) method Option::unwrap_unchecked Because all Rust's methods can be called as free functions, you can literally write Option::unwrap_unchecked for the same behaviour, or you can some_option.unwrap_unchecked() (in both cases you will need to be in unsafe context for this to be allowed and should write a SAFETY comment explaining why you're sure it…
Re: Rusty.hpp: A Borrow Checker and Memory Ownership System for C++20
#149Earlier quoted context omitted.
If 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 t3 = std::m…
> 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 wou…
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?
Re: Rusty.hpp: A Borrow Checker and Memory Ownership System for C++20
#150Earlier quoted context omitted.
> 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 wou…
> 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…
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 is roughly (getting rid of some irrelevant details):
fn index(&self, i: usize) -> &T
This is shorthand for fn index(&'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.