Date: 2024-06-20 02:36 pm (UTC)
simont: A picture of me in 2016 (Default)
From: [personal profile] simont
? operator being nice and short: of course, if someone is coming from a language that has exceptions, the ? operator is one more character than they're used to – it's the analogue of "just not bothering to catch an exception" so that it propagates silently out of the function!

(My favourite silly joke about Rust's error handling: "try not! Result<Do,DoNot>. There is no try.")

I was initially suspicious of the ? operator because I wasn't sure how I'd feel about having such a strong incentive to choose my function boundaries to correspond to the units of functionality that make sense to abandon half way through if something goes wrong. (You can do things differently if you need to, like making and immediately calling a closure so that the scope of ? is limited to that closure, but it would take a lot for you to think that was less ugly than the alternative :-) But in fact I haven't had trouble with that at all; it almost always seems entirely natural.

Perhaps less obvious: Rust errors and C++ exceptions have very different performance characteristics. The C++ exception system is designed on the assumption that exceptions are rare – thrown only in case of some unusual disaster. So the stack unwinding is horribly complicated and expensive in performance, but on the other hand, not throwing an exception has very low cost. Whereas in Rust, if even a successful Result is returned through a nest of 10 function calls all using ?, every level must manually check it in case it's an error, so there's more of a cost to not failing – but the cost of failing is much lower, because you just check a variable you have right there already, and don't have to hand off to a huge terrifying piece of stack unwinding code which consults huge compiler-generated tables. In C++ it would be a serious performance mistake to casually use exceptions for normal control flow (as you might in Python, say, where a StopIteration is thrown at the end of more or less any for-loop); in Rust it's probably fine.

On move vs copy constructors: a recurring theme is that Rust checks things at compile time that C++ can only check at run time. The reason std::move is less useful is because the C++ compiler isn't statically tracking which variables currently have live values in, and which don't. So in Rust, after you move out of an object, the compiler knows that variable doesn't contain a thing at all any more, so that it's a compile error to try to use it. But in C++, if you move out of an object, it's still legal to try to use it, it just won't do anything useful – and the compiler can't change that.

(Unless it will! Writing your own types in C++, you could always choose to write the move constructor/operator so that they leave the moved-from object in some predictable and useful state, like an empty list or a null pointer or zero or something, and then depend on that behaviour in client code. The STL types generally don't – they leave the object in a state where the only safe things to do are to assign something new into it or to destruct it. As far as I know the C++ standard doesn't even guarantee that moving out of unique_ptr leaves it containing nullptr, although this will often be what happens in practice because it's hard to imagine a C++ library implementing it any other way – so it's easy to accidentally rely on that not-guaranteed thing...)

Another example of compile- vs run-time checking is that C++'s analogue of Rust Result<T,E> is std::expected<T,E>. Both try to enforce that you don't accidentally 'squash' errors by just never remembering to check them (although in both cases there's a way to squash an error on purpose if you really meant to). The C++ std::expected object does it by having a check in its destructor so that if it had an error in it and you didn't extract or inspect the error (err, somehow) then you get a crash – but that means you still don't find out about the mistake until you have a test that exercises the failing code path. Whereas Rust will statically check at compile time (I think via #[must_use], unless I've misspelled that), so you find out without having to have exhaustive tests of your error handling. And that's a huge improvement, because error paths are notoriously hard to write tests for!
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

If you are unable to use this captcha for any reason, please contact us by email at support@dreamwidth.org

Active Recent Entries