The difference is that the use of the smart pointers is checked of the compiler
I've got a pretty big C++ codebase for my hobby projects, sanded down, polished and perfected over the years without the usual corporate pressure to ship. The few times I run into memory corruption, a memory leak, a segfault, dereferencing a shit pointer, undefined behavior, and so on, its always, always because I'm doing something I shouldn't be doing. Like working with raw pointers or pointers to pointers to pointers, or traversing an array of bytes to do something there's already a library that does, or manually calling delete on something, or using reinterpret_cast<>, or using one of the many footguns C++ happily gives me. The simple key is to just stop doing these unnecessary things.
It's not what C++ can or cannot do, it's not what Rust can or cannot do. It's what the Rust language and compiler opinionatedly encourage you to do and not to do. This is what's lacking in C++.
You've never used an out-of-bounds index? Accidentally used an object that was on the stack beyond the function call? Let an integer overflow? The problem with C++ is that all of these things don't look like unsafe operations, and people end up making mistakes with them that are not obvious.
Integer overflow can be turned into defined behavior through a compiler switch.
Holding pointers to stack variables past the return is not as easy to inadvertently do as an off-by-one. Someone has to store the address into a variable and this kind of code should raise alarm bells.
A more indirect way is to pass an address of a local variable to a function which is typical. But then if a function takes arbitrary pointers it receives and stores them, this should also raise some alarm bells.
Can't speak for the OP, but I never see any of these bugs in my C++ code. You can write code with these bugs but that is more of a style choice, practically speaking. The C++ bugs I see are almost always in complex state logic or in rarer cases an unexpected/unhandled error case from a call to outside code, which can happen in every language.
The question is how often you mess up in each language and how bad those mess-ups are. This depends a lot on the programmer(s) and the kind of project. My personal view is that the space of team/project combinations where you should ever start a new C++ project is now confined to "team desperately wants C++".
I think even now the best resource to learn C++ is to first learn C, then original C++, then all the modern memory management techniques, which is just crazy hard for a new programmer compared to just going through the Rust book 10 times (which is needed to get a deep understanding).
I'll let you decide whether this is sarcasm - I could go either way.
I'm with you, my sunken cost is 10 years, though.
Types that implement Copy are still moved. Copy just means “the original value is still usable after move”.
Makes me wonder out loud if the C++ community isn't going to add some kind of std::borrow/std::return_borrowed to the language. If that's even feasible with the type system and reference system as it is today.
nobody with an inch of nous does that anymore.
Especially when performance matters (eg: high frequency trading systems, video games, constrained embedded systems, and more), the improved locality that you can get from custom memory management can be worth it all on its own.
For a good talk on this subject, see Lakos "Local Memory Allocators" presentation:
* part 1: https://www.youtube.com/watch?v=nZNd5FjSquk, part 2: https://www.youtube.com/watch?v=CFzuFNSpycI
Perhaps the only hard part is storing and passing references everywhere, which may mean that one has to act like an automaton and patiently type their lifetimes to the Rust compiler. Unfortunately the Rust programming community has settled exactly on this kind of reference usage.
Rust is a huge win for the small niches where the GC overhead is unacceptable, because it is completely novel in its memory safety, which is absolutely a must and was neglected for way too long, but I never really understood the desire to use it for CRUD apps and the like. Sure, to each their own, but it is just an arguably bad choice. Even OSs could be written in managed languages - it’s not like it hasn’t been done before and they can just as well have escape hatches like Rust’s unsafe (hell, they are likely even safer if they are only used to manipulate “external heap”)
I suspect it was just happy circumstance that a type system so strong it makes memory access compile time checkable is also strong enough to eliminate the overheads of GC. So they did that too.
But the claim that all this tediousness is there to eliminate GC sort of misses the point - that's not the reason they introduced it. Rust's complex type system is a type of formal proof system that eliminates a lot of bugs at compile time. The "sum types" discussion above talks about another class of bugs it eliminates. Eliminating bugs at compile time is the point - not making memory allocation easier.
And to be honest, Rust doesn’t have all that strong type system compared to Haskell/ML which introduced these concepts in the first place. Also, there are plenty of languages in the category of “managed, with a strong type system”.