|
I've used both, and I'd say it's still quite a difference. There's many examples, but one I stumble on often is functions that accept a std::string_view, and return a part of that string. It's quite dangerous to make it return a std::string_view as well, especially given how easy it is to create temporaries (e.g. get_word(sentence1 + sentence2) would return a string_view to temporary data). Similar story with functions that return element references out of collections, functions that choose an element out of two, etc. In Rust on the other hand, you can get very reckless with very long chains of functions that return references out of other references, especially with parsers, iterators, and lambdas/closures. The Send/Sync traits for thread safety are also unmatched in C++, tagged union pattern matching will also heavily restrict invalid states, and so will Rust's affine(ish) types, which force a single owner for resources, as opposed to C++'s somewhat hard-to-use move semantics. The standard library also prefers safety over speed by default, e.g. with std::string_view's indexing being UB, std::optional's dereference being UB, and so on. |
- No reference variables in user code (reference parameters are OK.)
- Any classes that hold reference member variables can only be instantiated on the stack. The objects the references refer to must either be stack objects or reference parameters to the instantiating function.
- For durable links to sub-objects, use handles (tuples of std::shared_ptr to the main object plus a sub-object link or identifier.)
- Alternatively, require externally-accessible sub-objects to be held in shared_ptr's by the main object. They can then have superset lifetimes when needed.
I'll stick to the memory safety topic, but similar solutions exist for some of the other topics you mention, e.g. by avoiding std::optional.