but maybe thats how we'll finally get rid of c++.
I mean, this is kind of why C++ is successful. It's a toolbox that'll do anything and you can write high level or low level code.
> but maybe thats how we'll finally get rid of c++
Lol we're never getting rid of it. There's too much C++ software and will Rust replace it all? Probably not.
The issue is each codebase has its own DSL?
That is pretty close to the case with all non-C++ langs: industry settles on a handful of frameworks which are syntactically similar but each has their own gotchas.
It's not "an issue", it's in my opinion the very best development methodology there is: develop eDSLs for every sub-problems in the system
even then what does it mean to "know c++"? Do you mean the spec? Specific implementations?
IMHO the language and implementation have been far from simple for almost a decade now. I don't think that's necessarily a negative though.
I feel like you didn't read your own comment here. What you've described is a world that is littered with so many c++ variants, it will be impossible to eradicate them.
This is actually possible for C++, if we add a concept of pure functions: functions that don't modify anything indirectly reachable from arguments (or globals). Basically:
* Annotate those parameters as "immutable" or as coming from the same immutable "region". Anything reached through them would also have that immutable annotation.
* Anything created during the call would not have that annotation.
The type system, basically a borrow checker, could keep them separate and make sure we don't modify anything that came from outside the function.
We're currently adding this to Vale [0], It's a way to blend shared mutability with borrow checking, and it gives us the optimization power and memory safety of borrow checking without all the constraints and learning curve problems.
[0]: https://verdagon.dev/blog/seamless-fearless-structured-concu...
The issue is that a bunch of existing code actively holds and uses multiple mutable references to the same objects. It would simply not be able to adopt the chosen scheme, regardless of how it's spelled.
Don't get me wrong, it's not the only missing piece for C++; C++ lets pointers escape to other threads which might modify the objects while our thread considers them immutable. Vale solves this by isolating threads' memory from each other (except in the case of mutexes or Seamless Concurrency). Luckily, there are plenty of schemes that can solve that particular problem for C++.
If I had infinite time I would love to figure out how to implement this into C++, after the proof-of-concept in Vale. It's a fascinating topic, and an exciting time in the memory safety field, full of possibilities =)
Both const and immutable attributes are transitive, meaning they are "turtles all the way down." People who are used to C and C++'s non-transitive const find it a bit difficult to get used to; people who have used functional languages find it liberating, and it makes for much easier to understand code.
If there was a "imm" keyword in C++ which acted "deeply", that would get us pretty far towards our goal here. However, we'd then find ourselves in cases where we need to (for example) cast from an imm Engine* back to a (non-const) Engine*, often for values returned from functions. That's what this new "region borrow checker" concept would solve.
Immutability is not so simple in C++ as it might first appear.
E: Actually it might be GCC that has it...
I’m grudgingly coming around to the idea that Rust is probably a rock-solid FFI story away from being a serious part of my kit (pybind11 good, not like anything we have now).
But there is this in-between place where first-class linear/affine typing could be bootstrapped off of move semantics.
I have a fair amount of experience with `cxx`, and even with `autocxx`. These tools show a lot of promise but are nowhere in the ballpark of `pybind11` in features, maturity, flexibility, or ease-of-use. It's early days on the Rust <-> C++ journey, which I appreciate might not be a huge concern for a lot of folks, but I depend on a lot of existing C++. My in-house thing ("alloy") is a big mess of `bindgen`/`cbindgen` and pain. It works ok but it's super counterintuitive at best that I budget like 5x the time to hoist something core up into Rust and I do for Python.
I'm excited to see what happens in the space!
Check out PyO3: https://github.com/PyO3/PyO3
It also makes sense because in Spanner, doing any minor change required many months of review, because it was such critical infrastructure. Refactoring was a non-starter in a lot of cases.
So, a more gradual approach, just adding annotations that don't themselves affect the behavior of the program (just assist in static analysis) makes much more sense.
> We are designing, implementing, and evaluating an attribute-based annotation scheme for C++ that describes object lifetime contracts. It allows relatively cheap, scalable, local static analysis to find many common cases of heap-use-after-free and stack-use-after-return bugs. It allows other static analysis algorithms to be less conservative in their modeling of the C++ object graph and potential mutations done to it. Lifetime annotations also enable better C++/Rust and C++/Swift interoperability.
> This annotation scheme is inspired by Rust lifetimes, but it is adapted to C++ so that it can be incrementally rolled out to existing C++ codebases. Furthermore, the annotations can be automatically added to an existing codebase by a tool that infers the annotations based on the current behavior of each function’s implementation.
> Clang has existing features for detecting lifetime bugs [...]
https://docs.google.com/document/d/e/2PACX-1vRZr-HJcYmf2Y76D...
> If there are multiple input lifetimes but one of them applies to the implicit this parameter, that lifetime is assigned to all elided output lifetimes.
In Rust we have self rather than this in methods, but importantly we sometimes don't take a reference here, and that's still a method, and you still have the self parameter, but the lifetime elision rules don't apply. They don't apply because if you've actually got self, not some reference to self, the lifetime of self is going to end when the function finishes, so returning things with that lifetime is nonsense.
This can make sense in Rust for transformative methods. If there's a method on a God that turns them into a Mortal, the God doesn't exist any more when that method exits, the Mortal is the return type, and if you just drop it then I guess sucks to be them. (In Rust, and I think in C++ you can label something as must-use and cause a warning or error if the programmer forgets to use it).
It seems though, as if this C++ rule would hit such methods too. Can you distinguish in C++ between "a reference to me" and "me" when it comes to methods? If you had a method on a God class in C++ that transforms the God into a Mortal and returns the Mortal, what does that look like? Does this elision rule make sense to you for such a scenario?
This will change in C++23 with "explicit this", but then the `this` parameter will no longer be implicit and that translated elision rule will no longer apply either.
So in today's C++ your example might look something like this:
struct God {
Mortal transform() && { // Take an rvalue reference for `*this`, I guess?
return Mortal{ /* Move some fields out of `*this` probably? */ };
}
};
The elision rule does apply here even though we're moving out of `*this`, but again we are still taking a reference and the God object remains valid afterward anyway.With "explicit this" you could instead write something like this:
struct God {
Mortal transform(this God self) { ... }
};
Now there's no reference and the rule ceases to apply. (Though in most cases you are still going to be leaving a valid, but now at least unrelated, God object behind- the only way to avoid that is to construct it directly in argument position so it stays a prvalue.)Even if your team is more progressive, there is likely a huge mountain of still active legacy code that nobody wants to rewrite. There a more incremental approach is still the way to go.
Otherwise yes, for a greenfield project, rust is likely superior in many other ways. Not just in this area of borrowing but also build, syntax and lots of other features.
Lifetimes are not like an `unreachable` UB operation. Instead they are just a description of cross-function information, about what both the caller and callee are allowed to assume.
You could technically (both in Rust and under this proposal) do the same checks without them, if you had access to the entire program at once. However, this would be more expensive, and probably give you less-localized error messages (a lot like C++ template errors, for similar reasons).
An example: https://github.com/llvm/llvm-project/blob/main/clang/test/Se...
More details can be found at https://herbsutter.com/2018/09/20/lifetime-profile-v1-0-post...
Unfortunately it was never upstreamed according to https://github.com/mgehre/llvm-project/issues/98
I've not generally been a big fan of where C++ has been going in the last couple revisions, mostly because it seems the newer features aren't fully thought out when it comes to how they interact with other features (reminds me of javascript in that regard) leaving a bunch of new footguns, looks at lambda's.
I think i've said this here before, that C++ needs its own version of "use strict;" that basically kills off some of the syntactically odd corner cases that lead to those footguns.