There are a number of areas in programming where I'd always choose C++ over Rust - gameplay programming, retained-mode GUI programming and interpreted programming languages to name a few have very complex circular memory models that are somewhat solvable with weak_ptrs or refs stored in member variables passed through constructors but would be absolutely obnoxious to deal with and get right with the borrow checker.
Rust has Arc and Weak that can be used for solving it in a similar way C++ does. The primary difference is forcing usage of Mutex to avoid a large class of data race issues IMHO, and that's what makes it harder to get right. Async in Rust is also more mature than C++ coroutines. So there's also that.
Arc/Weak can only get you so far. The problem I have with Rust in the mentioned domains (gameplay programming, retained GUI, interpreters) is what makes it so good, sharing xor mutability. If you're writing lots of multi-threaded code it's fantastic. If your problem domain is like the ones mentioned it hurts you.
These are all highly non-parallel problems. They don't gain much from being parallel, and because Rust imposes 'restrict' semantics on even single threaded code you end up making it much harder to write code in these domains.
This has been my experience with Rust. Shared mutability is safe on a single thread without 'restrict' on all your pointers, and Rust has limited options to opt into shared mutability (with lots of ergonomic caveats).
Don't get me wrong though, I still think Rust is a great tool. It just has tradeoffs.
> Rust has limited options to opt into shared mutability
The idiomatic Rust equivalent of a C non-restrict pointer is arguably &Cell<T>. The biggest problem with it is library code that takes &mut T when the likes of &Cell<T> might suffice (because potential aliasing does not affect the semantics of what that Rust code is doing), but this is an acknowledged problem and the Rust project will take pull req's that fix it where it occurs.
Shared mutability can cause race conditions in single-threaded environments as well, if you need asynchronous contexts where mutations can be interleaved between suspension points. Think of events, message passing channels, I/O, timers, ticks...
If you're sure you're never going to need multi-threaded environment, you have an option as well: Replace std::sync with std::rc, Mutex with RefCell in the above toy example and that's about it.
If you want to use some asynchronous runtime, replace std::sync with tokio::sync (or std::rc), slap async/awaits along with a single-threaded runtime and that's about it.
Of course, the code above is just a toy example and business logic is much more complex in real world, but compare this to what it would take to write same C++ logic in async.
I found Rust's approach massively more ergonomic compared to C++ approach of passing closures around for asio-like IO contexts or coroutine compiler-magic which opens new novel avenues to shoot myself on the foot, well, to the extent I could grasp it.
It's true Rust forces you to pay all this cost ahead of time. It's also true most applications don't require this level of safety really, so it becomes ridiculous to pay it upfront. And even for some that require such high level of safety, you can skip bunch of bolts on a plane door and it will still be a billion dollar company at the end of the day, so...
Yes, you could just use Arc everywhere. But then you've spent a large portion of your coding time fighting the compiler and wasting lines for something that will essentially always be single threaded. (a UI event loop)
The C++ solution would be to start the threads and use an MPSC queue (which, ironically, Rust also has) in order to update the UI.
Rust will eventually stumble upon ergonomics and allow portions of code to be specified as single threaded or embarrassingly parallel, but unfortunately the community evolved horse blinders early on and isn't letting them go any time soon.
> Rust doesn't have computed goto (gcc/clang extension heavily used in interpreters like CPython and LuaJIT for direct threading).
On the other hand, there's the recently-added-to-nightly `become` for guaranteed tail calls, which might work better than computed gotos if CPython is a good example [0]
> When using both attributes [[[clang::musttail]] and preserve_none], Jin's new tail-call-based interpreter inherits the nice performance benefits of the computed-goto-based version, while also making it easier for the compiler to figure out optimal register allocations and other local optimizations.
To be fair I don't think Rust has a preserve_none equivalent yet, but given naked functions are a thing in Rust I'd hope it isn't too bad to support?