All the rules about when you can safely use/share this thing and who needs to deallocate this thing becomes a pain to figure out throughout the code base.
With rust, because ownership is baked into the type system, those classes of problems just don't exist. Change it from a `Foo&` to a `Box<Foo>`, no problem. The compiler will blow up and tell you exactly where (and how) to fix those things.
This applies to more than just memory allocations. Imagine calling a method from 2 threads that manipulates shared memory. Rust won't allow you to do that without protecting that memory with something like a `Mutex`.
C++ can never quite get there despite all the innovations made because of its descent from C style types, which are much more simplistic.
Use of maximal type safety in modern C++ is astonishingly robust and expressive. If it compiles it generally works. The majority of developers are a bit lazy about type safety in my experience, which works against you. It depends on the org.
Rust in theory has a lot of potential here because it's designed to minimize action at a distance.
(Many newer C++ code-bases discourage relying on the preprocessor, but that only goes so far.)
I would say C++ is mostly the same experience, with the exception of anything involving memory safety & lifetimes. It can't guarantee you didn't accidentally introduce a use-after-free/move or anything else of that nature.
In a tortoise and hare race, sprinting off quickly doesn't count if you can't reliably reach the finish line.
That said, there are some general themes:
- The lack of implicit magic in the language. Destructors (specifically the fact that they are called automatically) is implicit. That's it.
- The context-free nature of the language. This exists at several levels:
1. At a function level. There is no cross-function inference and all the information about a function is in its signature, so you don't need to look anywhere else to understand what it does.
2. At a module level. Everything you use from elsewhere is explicitly imported in the module. There's nothing that can change what a piece of code does across a crate (eg. the way you can just redefine `true` to something else in C/C++.).
3. At a file level. The module hierarchy is explicit, it is not inferred based on the layout of files on disk.
Together, these things means you can look at a piece of Rust code and understand it in isolation, without even needing to be familiar with the codebase as a whole. You can move a piece of code from one location to another and (aside from fixing imports) can expect it to continue to function exactly as it did before.
- The amount of information that is available to the compiler. The rich type system and lifetimes means that it's harder to mess up a refactor without the compiler detecting it.- The fact that it can be used as a safe language. This is not unique to Rust, but Rust is unique in being suited to the lowest-level tasks whilst still being safe. Safety limits the damage a bad refactoring can do (at worst, you break the code being refactored, but at least you won't give the whole program UB or cause a segfault).
- The fact that macros operate on the AST rather than text. This eliminates a class of bugs with C-like macros, where implementation details of the macro have a tendency to leak through the macro definition. Refactorings can often trigger these kinds of bugs because you cannot easily think at that level while doing a large refactor.
- The tooling. Whether it's quickly knowing the type of a variable, swapping out a dependency, or applying broad fixes to a codebase, the tooling works extremely well together.
- Consistency of the language. Everything is an expression. "void" is just a normal type, etc. This level of consistency means that large refactorings require fewer actual changes than they might do in other languages. For example, when I used a bit of C# again after using Rust, it was very noticeable the amount of "pointless" transformations I had to do when refactoring because making an expression conditional may require introducing a new variable because `if` statements cannot be used in an expression. (Yeah there's the ternary operator, but it can't be used in every case, and that's just another example of inconsistency).
Well, destructors and unwinding. Many, many bugs in unsafe code have been found through unwinding code paths being not fully accounted for. At least in safe code that doesn't use catch_unwind, the worst that can happen is an unexpected state of an object shared across threads.
What's even more fun is the combination of the two. For instance, the catch_unwind function has a big flaw, where it returns an arbitrary Box<dyn Any> to represent the panic payload, and if you aren't very careful about dropping that object, its destructor can panic and start unwinding again. (There are plans to fix this by putting the payload in a shim type that makes the destructor abort on unwind, without actually changing its type_id. I find it quite an ugly hack, but there's not really any other way around it.)
(There are also other ideas to remove language support for unwinding destructors entirely. I'm against that for a few different reasons, but I'm not the one running the show.)