I've been thinking about a subject for a while now, and I'd like to have your opinion.
Rust has a lot of advantages: it's not just about borrowing rules.
But wouldn't it be interesting to add borrowing rules to C/C++ to avoid manual allocation and deallocation?
let me explain:
if, for example, we compile C/C++ code to LLVM, then add a Compiler borrow to the chain that will take this intermediate code (LLVM) and check the borrowing rules and add deallocation automatically.
Exactly. The concept of lifetimes cannot be expressed only by the work made by the compiler; it has to be part of the type system and thus cannot be retrofitted to C/C++ as is.
There are multiple ongoing efforts in multiple directions such as making the compiler smarter so that it can better infer lifetimes, as well as initiatives to have something similar in C++.
It's not for the lack of trying, people have been trying to make those languages safe(r) for decades. Rust started in 2006 and then open sourced by Mozilla in 2010. That's 14 years ago at the time of writing this. That should give you an idea on how difficult and time consuming such an endeavour would be.
The borrowing rules are not made to avoid manual allocation and deallocation. Those are handled by the RAII pattern, which C++ has in the form of destructors and that's what inspired Rust's version (Drop). The borrowing rules instead ensure that you don't have dangling references.
The borrow checker needs to know what has a lifetime and how the inputs and output lifetimes of functions are related to each other. Meanwhile LLVM IR is mostly untyped and has none of those (and similarly C++ has no lifetimes nor lifetime annotations), so how are you going to do borrow checking on LLVM IR?
If you have some great ideas as to how to make C or C++ have the same memory safety as Rust then please do present them to the standards comities of C and C++. I'm sure they will be happy to hear them.
For sure I have heard Bjarne Stroustrup himself saying "We can do memory safety in C++ as well as Rust and even better" in one of his C++ conference presentations.
Meanwhile, many have bailed out of the idea. Including the creators of Rust.
On the contrary: lifetimes are entirely independent from everything else in Rust[1] and there are even existential proof.
Problem with topicstarter's proposal is entirely different.
Why do you even include borrows in that picture? Deallocation happen in Rust entirely automatically without any needed for borrow and lifetimes system! Existential proof. Just like in C++.
I don't think you realize what borrow checked and lifetime system represents in reality. No, it's not something that affects the generated code[2].
No, it's built-on proof of program correctness (lifetimes markups and types) plus theorem verifier / correctness verifier (borrow-checker).
It's possible to add one to C++, not that hard, even, can even be done the way Ada people did that, via special type of comments, but then… what? Someone have to go and rewrite all C++ code that exists to add appropriate markup!
And if you are planning to do that then why not just rewrite it in Rust, instead?
Again: it's not that hard, just pointless. If you think about C++ today is akin to COBOL: it's very important and would be important for many years to come… because there are billion lines of code written in it.
And every plan which brings safety to C++ also brings the need to rewrite all that code. And if you are rewriting it anyway, then why stick with C++?
Ada is different: it's mostly used for small embedded systems where full rewrite is not considered crazy hard problem. That's why Ada picked up borrow-checker years ago, but C++ couldn't do that.
What you say makes sense, the biggest obstacle is Lifetime, if only there was a way to do without it in Rust too.
for example, analyze llvm code and generate an execution tree, and detect when a memory zone is not accessed and add llvm deallocation code.
But it's true that without Lifetime this is difficult, if not impossible.
it's just a thought, basically I don't like C/C++ very much, especially with the .h, but sometimes with this kind of thinking you realize even more the value of Rust.
I think that the borrowing rules limit the risk of having dangling pointers, and therefore make it easier to locate de-alocation zones. (maybe I'm wrong)
(De)allocation is not the problem solved by borrow checking AFAIK.
Every object with automatic lifetime is deallocated when goes out of scope.
Objects stored on the heap are either manually controlled (in case of "wild" pointers with new/delete) or managed with smart pointers (shared, unique e.t.c.) which are much like Box and Rc. Smart pointers were introduced in C++11 and since then they are recommended approach. Most people use them and do not delete anything manually. You do not need to use new/delete in C++ for anything but very low-level things.
You could always store everything on the heap and use ref. counter based pointers (as they do in ObjectiveC for example), but:
That might be slow as you need to increase/decrease counter
Reference cycles lead to memory leakage
Some functions do not need to own object, they just need to borrow it for a while
This is why C++ and Rust have references which do not own object
The problem is reference (and pointer of course) could outlive object they point to because compiler doesn't check it.
void do_all(const std::string& strRef)
{
// Can I save strRef in the field with static duration or object field?
}
In Rust, each reference has lifetime checked by compiler. In C++ it doesn't.
This is the main idea of borrow checking as I understand it.
Adding lifetime to CPP references will break all code written in the last 35 years rendering C++ unusable.
Rust also checks that you can't have two mutable references to the same object, mutate it from 2 different threads (because that would lead to race conditions) which is also not checked by C++ and can't simply be added.
I'm not sure it's possible with Lifetime and even if that were actually possible with Lifetime that's not what Rust and C++ are doing.
Even the name RAII which Rust tutorials are using comes from C++! You couldn't add Rust rules for object dealocation to C++ because they are already there!
They absolutely do that but they are not doing in the compilation time! Rather, human, by adding lifetime annotation, does that and then compiler verifies correctness in a separate compilation pass which doesn't affect anything else[1].
You still seem to think that borrow checking has something to do with allocation/deallocation: it does not. It only "checks" if the program satisfies some properties.
Doing this kind of analysis without lifetimes is not impossible [1], but to make it work you always have to assume the worst case, which will make it reject a lot more programs, making this checker useless due to many false positives. Lifetimes actually allow you to accept more programs thanks to allowing the borrow checker to make more assumptions.
The C++ Core Guidelines Lifetime Safety Profile is a spec for Rust-style borrow checking in C++, including optional Rust-style lifetime annotations for types and functions. As in Rust, there are default rules that apply when lifetime annotations are elided. As in Rust, it is a static analysis that does not affect codegen or allocation/deallocation semantics.
(I personally doubt that it’s practical to reach the promised level of lifetime safety in C++ without significantly more work than initially envisioned. Stroustrup and Sutter first released the C++ Core Guidelines and demonstrated prototype checkers in 2015. Nearly nine years later, as far as I know, neither Microsoft nor Clang’s checkers can prevent even fairly simple use-after-free cases that the Rust borrow checker has handled from the beginning.)
This is precisely what I'm talking about: to handle these cases you need information about lifetimes of variables that come in function and then come out.
And that's precisely where the whole thing falls apart without full rewrite: either you allow compiler to do analysis after inlining (and then you produce bazillion warnings on any existing C++ code which would be promptly silenced as useless), or you don't do that (and then have pathetic error detection rate).
The solution is to add markup on functions… but who would do that?
According to him, it should be backward compatible with C++. However he had to change the object model and value types, moves become destructive as well.
He also adds 2 new keywords: safe and unsafe.
The circle compiler is closed-source for now but can be downloaded for linux and macos AFAIR. It’s basically a from scratch C++ frontend for LLVM. The latest lifetimes work was not yet released last time I checked.
Well, I think I have already told this, but it's still true: 10 years ago it would have been something with high chance of success and 15 years ago it would have made Rust DOA.
Today? It's curiosity at best. To get full benefits from it you would still need to rewrite everything and why start doing that now from scratch when you have lots of nice foundational Rust crates already?
Because you don't need to annotate everything. You only need to annotate the API surface you actually use, and anything underneath that can remain unchanged without impacting your usage.
They're not direct analogues, but you can look at the addition of nullability annotations to JVM libraries (Java/Kotlin/Scala) and CLR libraries (C#/F#), or the propagation of TypeScript .d.ts files describing JavaScript libraries' effective API types. For closer to a "rewrite the entire API surface" example, look at Apple's migration from ObjC to Swift. There wasn't any new type information being added IIRC, but essentially every single symbol in the OS API got an annotation mapping from ObjC name to Swift name (or just deprecated).
The benefit to "successor languages" is in near-seamless interoperability. The entire thesis of Rust is that there's a benefit in having a safe veneer over an unsafe implementation. If circle can make it possible to continue to use existing C++ libraries while keeping new code meaningfully safer and incrementally annotating existing code, that's a significant benefit.
Rust fundamentally can't compete on using C++ libraries; cxx still requires significant manual effort to bridge C++ templates to Rust generics. Rust can compete on using (nonmacro) C bindings, but essentially any language people actually use can communicate across the lowest common denominator that is the C ABI.
At this point it's race with question who would finish first: Herb Sutter (and others) with their “subset of a superset” approach or guys who try to find a way to bridge the gap between C++ and Rust.
And bridge approach have clear advantage: at least their target language exists!
Safe “subset of a superset” C++ doesn't exist and who knows how long would it take to bring it to some sort of completion?
Crubit is currently targeting “calling FFI-friendly C++ from Rust”. This is a very simple subset of the problem of C++ interoperability, and doesn't address the issue of needing to rewrite the interface at all. Generating the shims from the C++ source/headers instead of from a Rust description is lower overhead than using cxx directly (although automating such is the idea behind autocxx, also a Google-owned project, as a "bindgen for cxx"), but does nothing with respect to the manual effort I'm alluding to in interop for code using any actual C++ API design functionality (thus not being trivially FFI-friendly).
Even that subset is still a huge boon, though, for the exact same reason I give to successor languages. But a successor language will, all else being equal (it rarely is), always win on interop with the parent ecosystem, due to being designed with that specific use case in mind.
And how long do you think would that be the limitation?
And why do you think they started Crubit if they already have autocxx?
The answer is obvious, if you'll think about it: autocxx/cxx could never handle what Google really needs: support for template-heavy libraries with unstable API.
That's precisely what I'm talking about. Google is obviously playing with ways to handle template-heavy C++ in their interop (if you saw Google-style code in Abseil, Chrome, etc you would understand that anything else wouldn't help them) and is, most likely, months from being able to roll out some alpha-quality version with more robust version in a year or two.
And “subset of superset” guys are not even at the reasonable stage with their definition of the target language!
Sure, they started talking much earlier, almost decade ago… but it's only in Achilles and the tortoise fairy tale tortoise may stay ahead by simply starting earlier.
P.S. We actually know, approximately, when Crubit would have to deliver support for templates. It's either this year or next year. Otherwise Carbon would win the race. But, ironically enough, even if Carbon would win it would still not be “subset of superset” C++.
Would be nice to see some working proof from mr. Stroustrup. Haven't seen one yet, just some quite ugly attempts with template-based annotations consumable by external checker tool which doesn't exist
C++ has too many pitfalls, dark corners and UB. I guess that's the root cause.