That's not how it works (unless you're using 32-bit Windows). I wrote a long explanation using an old gcc on Linux at https://stackoverflow.com/questions/307610/how-do-exceptions...
Basically, calling a function that can throw doesn't save any information (other than what it already has to save to do the call), and it doesn't check any return value (other than any check on the return value you wrote yourself). Instead, the exception machinery uses compiler-generated tables, keyed to the return address, to direct the execution flow to the correct landing pad.
As a basic example:
x = 1
something_that_can_fail()
x = 2
With unwinding, the `x = 1` write must be preserved if whoever catches can observe it (and if that's unclear, it must be preserved conservatively).The C++ STL riddled with complex machinery to try to work around this sort of thing because everything can throw. Copies, moves, you name it! Not even reallocating an array can be done with realloc unless non-throwingness can be proven.
If you try to make throwing "opt in" like Java, then you end up with nasty "throwingness polymorphism" problems and end up with constructs like Swift's "rethrows". By contrast, error propagation through normal values naturally composes because it's really easy to write code that generically handles return values.
Function calls themselves can force something like "x = 1" if x is observable from the callee.
In the case where "x = 1" is not observable from the catch block, then there is no effect. In cases where you would use "try!" in Rust, in the equivalent C++ cases the catch block wouldn't be able see local variables since it would be in a parent function call.
I think LTO largely eliminates this issue altogether since function bodies are visible to the optimizer in that case.
Error-case performance is negligible as it's much more rare (cf. Amdahl's law). If errors are common in a specific section of code, in C++ you have the option of using simple error-checking. You have no such counter-option in Rust.
You're moving the goal posts. Itanium processors do have branch prediction so comparing C++ exceptions on Itanium to Rust on embedded architectures is meaningless. In embedded Rust, the worst case scenario is a compare between a constant and a register or a byte on the stack. Most non-ARM embedded platforms I've written firmware for barely have a three cycle multiply so a one cycle compare will be negligible except in the most extreme of cases (where you'd just write inline assembly anyway, no matter the language). On those platforms, you spend more time restoring registers than comparing the result of a call. On ARM processors, conditional execution behaves like a branch predictor on short compare jumps so there's again no overhead (except with value types, in which case Option adds 1-4 bytes for the None case on the stack).
Either way, Rust's error checking overhead is irrelevant. If your embedded environment is so resource constrained that Rust's error checking is too much overhead for you, then you certainly can't use C++ exceptions. They are too unpredictable without prohibitively expensive physical testing and tedious tuning of worst case code paths (unless you don't care about hard real time guarantees at all). If it's not, then it doesn't matter whether you use Rust errors or C++ exceptions because the overhead is a rounding error. We're not talking about something like reading memory from cache vs RAM or static vs dynamic dispatch where the difference in performance can be 10-1000x, we're literally talking about single digit cycles here.
> If errors are common in a specific section of code, in C++ you have the option of using simple error-checking. You have no such counter-option in Rust.
Yes, you do. You can unwind the stack manually and jump to the error handler, just like the C++ compiler does for you with exceptions. This is how the panic macro is implemented so all you have to do is modify its implementation (compiler-builtins crate) and create your own catch_unwind function. With the amount of time you'd spend typing "if (result != null)" or implementing an option type in C++, you can write your own exception implementation using Rust macros and traits. Rust developers made the right decision: "simple" error checking is the default with syntactic sugar to ease its use and if you really need those few extra cycles, you can make your own crate or compiler plugin that will give you the same features without polluting the rest of the ecosystem with a default error handling mechanism that is opaque, complex, and unsuitable for resource constrained real time systems.
Rust's error checking overhead is not unanimously irrelevant, it's not uncommon for functions to be called in tight loops.
I won't address your other comments since they are subjective and/or dependent on circumstances.
Just because you don't have to write the code doesn't mean that it doesn't exist! Exceptions are not a zero cost abstraction. Every C++ functional call that can throw adds several instructions of overhead to save information about the call stack and check the return value so even when you don't have an exception you still have the same amount of overhead as all Rust errors. Options are simple types that can often be optimized to the size of T in Some(T) and they're on the stack so worst case is you need an extra usize slot in your stack.
In the case of success, Rust is at least as efficient with errors as C++ but far more efficient when errors do occur.