5 ms·
> This might be influenced by the fact that I've used C/C++ for much longer, but my impression is that Rust is an even larger language with more details to lear
by trealira 3y ago
> This might be influenced by the fact that I've used C/C++ for much longer, but my impression is that Rust is an even larger language with more details to learn than C++. The difference is that in Rust if you get the details wrong (in safe code) you get a compiler error or at worst a logic bug, while in C++ sometimes you get a compiler error and sometimes you get memory corruption or integer overflows, or undefined behavior.
I disagree. Rust seems less complicated in many ways. For example, move semantics are a lot simpler; they're always a byte copy, whereas with C++, you have to remember rvalue references, lvalue references, and universal/forwarding references (which are usually rvalue references but sometimes are lvalue references). You also have to be careful not to mess with a moved-from object, as it's in an unspecified state.
C++ also makes a distinction between trivially copyable types and non-trivially copyable types (a distinction Rust doesn't make). It's difficult to remember the rules for non-trivially copyable classes, but they boil down to "if it has a user-provided constructor, destructor, or copy-assignment operator; or if it has virtual functions; then it's not trivially copyable."
In Rust, all you have to remember is that types are by default moveable (and moves are a memcpy), deep copies are implemented through the Clone trait, and if your type can't be memcpy'd (e.g., a self-referential struct) it needs to only be accessible through Pin pointers, which ensure that it isn't memcpy'd in safe code.
Another thing: you can cause a use-after-free error by combining coroutines with lambdas [1]. An error like this only happens because C++'s rules around coroutines and lambdas are complicated enough that the committee didn't forsee this happening. This seems indicative of higher complexity in C++ than Rust, to me.
[1]: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=95111#c23 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=95111#c23
- pavon 3y agoOn the other-hand, I find it annoying in Rust that an assignment or an unadorned function parameter might result in a move or a copy and you can't tell by the function signature or call site. Instead it depends on whether the type implements the Copy trait, which is a big semantic difference based on "spooky action at a distance". > You also have to be careful not to mess with a moved-from object, as it's in an unspecified state. This is a great example of Rust being easier, even when it just as complex. Both languages don't allow you to use an object after it is moved, so it is the same amount to learn and to think about when writing code, but Rust will give you a helpful compile-time error while C++ will let you blow your foot off.
- kaba0 3y ago> but Rust will give you a helpful compile-time error while C++ will let you blow your foot off. But Rust can actually analyze trivially when this happens, while it is not possible with C++ due to language semantics.
- fluoridation 3y ago>You also have to be careful not to mess with a moved-from object, as it's in an unspecified state. This is incorrect, somewhat. It's "unspecified" in the sense that the standard doesn't mandate that user-defined move constructors and assignment operators leave source objects in any particular state. All standard library classes are left in well-defined states when moved (if they can be moved), and you can choose to define your classes to do the same. The usual rule of thumb is that a moved object should be in the same state as if it had just been default-constructed. This is what all the standard classes do. Move semantics in C++ and in Rust are more or less equivalent. The major differences are that C++ copies by default and rust moves by default, and that Rust doesn't allow using a moved object while C++ does. >In Rust, all you have to remember is that types are by default moveable (and moves are a memcpy), deep copies are implemented through the Clone trait, and if your type can't be memcpy'd (e.g., a self-referential struct) it needs to only be accessible through Pin pointers, which ensure that it isn't memcpy'd in safe code. If defining value semantics was so simple you wouldn't have had to bring up Pin pointers (which I assume are not just pointers, but something special that needs to be kept in mind), or the fact that Rust understands that there are two different kinds of code, which C++ doesn't distinguish. It's suddenly so obvious that one is simpler than the other. >Another thing: you can cause a use-after-free error by combining coroutines with lambdas [1]. An error like this only happens because C++'s rules around coroutines and lambdas are complicated enough that the committee didn't forsee this happening. This seems indicative of higher complexity in C++ than Rust, to me. It's trivially easy to cause use-after-free errors with lambdas. Return an std::function<void()> that captures and reads a local std::unique_ptr by reference and then call operator()() on the object. This is not a complex interplay between features; lambdas necessarily introduce dynamic lifetimes into a language that was originally not designed to support them, so using them requires care.
- trealira 3y ago>If defining value semantics was so simple you wouldn't have had to bring up Pin pointers (which I assume are not just pointers, but something special that needs to be kept in mind) [...] Pin<P> is basically a wrapper for any pointer type P, whether it's Box<T>, &mut T, &T, etc. Its sole purpose is to keep the object that's pointed at from being moved, byte-copied, or byte-swapped. For self-referential structs, that's all you can do; there's no equivalent to move constructors. See this link at the bottom [1]. > This is incorrect, somewhat. [...] You're right, but I really just meant that you have to keep track of more. If you use a moved-from unique_ptr, for example, you'll dereference a null pointer. Sometimes you do want to use a moved-from object, though, so it's not like this can be disallowed. It's something you have to keep track of. I just think this is more complex than Rust's unconditional rule that moves are bytewise copies, and moved-from objects aren't allowed to be used. I've heard C++ programmers complain about Rust being less powerful than Rust in this respect, but it is simpler. > It's trivially easy to cause use-after-free errors with lambdas. I don't fully understand it, but from what's described at the link, this causes the use-after-free bug: [x] () -> future<T> { co_await something(); co_return x; } The fix is to write it like this, I'm pretty sure: [x] () -> future<T> { auto xx = x; co_await something(); co_return xx; } The reason is because coroutines copy their input to their own stack frames, but lambdas are passed by address, and so the coroutine later dereferences a dangling pointer. Again, it just seems more complex than anything in Rust. [1]: https://doc.rust-lang.org/nightly/std/pin/index.html https://doc.rust-lang.org/nightly/std/pin/index.html