I suppose it depends on what you would consider being “serious” about memory safety.
I view move semantics as not a memory safety thing at all (in fact, use-after-move can be exactly as ugly as use-after-delete), and smart pointers as a “let’s just make things marginally better than raw raw new/delete” thing. Smart pointers in C++ can still be null, after all.
Calling C++ not serious about seems a reasonable take to me. Of course Rust didn’t invent all of its solutions from scratch.
Smart pointers + static analysis + sanitizers is the best options I am aware of for improving the correctness of C++ programs, and I don't think Rust really had much to do with them. The picture painted that Rust made the C++ standards committee scramble to start addressing safety seems wrong; it feels more like there's been a mostly steady increase in safety over time without need for any outside influence.
(a) Smart pointers + static analysis + sanitizers make C++ pretty usable
(b) C++ usability requires brittle, half-baked add-ons such as smart pointers and non-deterministic/heuristic/unreliable/NP-hard development tools such as static analysis + sanitizers.
OK. I think some people mistook me as making a value judgement about C++. This is not true, I am merely saying I have doubts that Rust has had a serious impact on C++ design (yet; if it does it will start showing up soon, though.) If you don't believe me please carefully reread my comments; I really wasn't trying to say anything overall good or bad about C++, just observation about trajectory.
My personal opinions on C++ are not very positive, but it's a language I literally grew with. My experience is that C++ code very gradually became more stable and less buggy. The thing is, some of that is just improved discipline, some of it is static analysis, etc. But a lot of it, is genuinely C++0x/C++11 and features that built on top of this.
So the idea that Rust showed up and suddenly C++ cared about safety? I don't really see it. I think C++ developers started caring about safety and then Rust showed up because of that.
P.S.: While the borrow checker and ownership semantics is really cool, I think a programming language's ecosystem and the 'best practices' it lends itself well to have a greater impact that people completely miss. Rust, Go, Zig are all clearly trying to foster a culture of writing more correct and complete programs. Error handling doesn't just feel like a couple control flow mechanisms you can use, but a responsibility that you have. Modern C++ is getting better probably more because of this than any committee; although I really wish the Expected type would've gone somewhere, since I feel Rust's `Result<...>` type and Go's multiple-return with `error` are being proven right as ways to handle errors robustly even with their caveats. (I've heard Zig's error handling is really cool too, but I haven't had a chance to write a serious Zig program. I'll get to it soon.)
When I think about C++ "smart" pointers, I often think about easy to provoke / difficult to spot fatal edge cases. E.g.
- C++ code where some method calls std::shared_from_this(), and that method ends up being called indirectly during the object's construction, leading to a 0-pointer dereference [1]
- accidentally creating two shared-pointers to the same object leading to an eventual double-free (that may just silently corrupt the heap while the program continues running)
- undetectable cyclic shared_ptr<> references somewhere deeply hidden in the code causing a memory leak
Modern C++ feels like a case of "but we can do that in C++, too" syndrome. Stuff that "just works" in Java or Lisp, can now be done in C++, too, however in C++ it will only work for 95% of all use cases, and break in a spectacular manner for the remaining 5%.
E.g. think about the template meta-programming madness (and efficiency WRT compile-time) and compare that with what Lisp offers (see also [2]).
Both of your first two cases exist when people convert code bases that used raw pointers to shared_ptr. The normal intended usage of shared_ptr is to use make_shared and never have a raw pointer ever exist. The need to us shared_from this (or create a new independent shared ptr) happens only because some part of the code base is still using raw pointers for that particular class.
OTOH, C++ needs the ability to support old code bases because of its age. Of course a new language can forego this.
As for cycles, for GC language using ref-counting under the hood, the same problem applies. I've also seen memory leak from accidental keeping of pointers in fully GC languages. I've seen it happens in Java multiple times. Of course, two wrongs don't make right, but in this case GC is only solving one case.
But the more fundamental issue at play is that pointer cycles usually means a lack of up-front design and thought about how a program will work. As such, it is bad, but only a symptom of a lack of proper initial design which can manifest itself in all kind of places.
I think for shared_from_this() I have a different use-case in mind: an object of some class wants to register with some other class. I.e. think about something like
auto self = shared_from_this();
document.onclick([self](){self->doit(); } );
I'm not sure there is a way to express that in C++ without use of enable_shared_from_this.
It is very easy to accidentally run this kind of code from a constructor where shared_from_this() is a 0-ptr.
make_shared also only helps in 95% of cases. E.g. it does not allow specifying a custom deleter, which is a use-case that is allowed by the shared_ptr<> constructor. It also won't fix any of the problems WRT shared_from_this() being called in the constructor.
> I view move semantics as not a memory safety thing at all (in fact, use-after-move can be exactly as ugly as use-after-delete),
Note that move-destructors were also considered for C++11, and were not added for lack of time / implementation experience . And this is really too bad, because they are at least as important as move constructors if not more so; I've yet to encounter a case where you actually want to use a moved-from object after the move.
I view move semantics as not a memory safety thing at all (in fact, use-after-move can be exactly as ugly as use-after-delete), and smart pointers as a “let’s just make things marginally better than raw raw new/delete” thing. Smart pointers in C++ can still be null, after all.
Calling C++ not serious about seems a reasonable take to me. Of course Rust didn’t invent all of its solutions from scratch.