I want to clarify this a bit. I agree that there are costs to panics, though I don't know if the runtime costs when you don't use panics are as notable as you imply.
(Or put another way, I would say the cost of unused panics are similar to that of bounds checking, which is in a similar position of in a perfect world, unnecessary.)
First off: panics are (intended to be) not an error handling mechanism, and anyone using them as such are making an explicitly nonportable decision.
Instead, panics are an alternative to aborting. Rather than taking down the whole thread or process with an abort, unwinding allows destructors to run and for the program to only take down the logical inflight task. Panics are (supposed to be) non-recoverable errors; the only way to continue after a panic is to trash that task and move on. For short-lived programs that's effectively an abort (modulo the drop cleanup), but for long-lived programs that is an important feature to have.
Also important is that while unwinding support is default, panic=abort
is a normal compilation mode for Rust. The cost of an untaken panic is primarily one of code size. On Windows, it is purely code size; the metadata to allow for unwinding is required by the OS, and only the landing pads can be omitted. On other OSes, you can have non unwindable frames, so there is a bit larger cost to setting it up, but still almost entirely just in code size.
When you set panic=abort
, Rust doesn't emit the unwind landing pads, so basically all of the language runtime costs of panics disappear. What remains afterwards is the checks to panic (and abort), and the implementation cost of being correct in the face of unwinding. If you know that you're only run with panic=abort
, you can just ignore unwind correctness. For a general purpose library, though, you need to be unwind safe, so we'll focus on that under the "don't pay for what you don't use" principle.
(And I want to note that writing code to avoid panics is at worst as burdensome as writing code to avoid UB in C or C++. Rust is a younger language, so there's fewer best practices and less tooling to help with doing so, but it's effectively the same. With some Code Crimes™️, you can even effectively just make panics UB (by making the panic handler call unreachable_unchecked
).)
Mutex poisoning is generally understood to be a library implementation mistake (well, suboptimality). Poisoning is not required for safety, and the parking_lot
primitives don't have any panicking. Rather, poisoning is a lint against potentially logically incorrect state, that can be ignored, even for the std mutex.
The better design would've been to have poisoning be a separate composable functionality, rather than baked into the design of Mutex
. It won't change the API, but perhaps we'll be able to put a const bool onto Mutex
to control whether it poisons in the future.
Only insomuch as they need to be unwind safe. If you're writing safe code, this is a non-problem, and you don't have to worry about it. If you're writing unsafe code, this basically falls to the "pre poop your pants" principle—any time you pass control flow back to downstream safe code, you should be in a safe state even if no more of your code is run. However, this is the case even without unwinding, as safe code can forget
to run your code.
The only case where unwind safety is separate from forget
safety is when you call a user closure (or trait impl, which does include basic operators on unknown types tbf). Here you are guaranteed that control flow returns to you, though it may do so via an unwind. The requirement is that you do your world fixup in a Drop
handler (or have an abort-on-drop bomb), which I may note is probably a good idea anyway, so it's much more difficult to accidentally forget to perform the fixup. The cost when your code doesn't panic is just that of developer cost to be unwind safe. Not even unwind correct (where stuff is all dropped correctly), I might add, just unwind safe (where nothing is double dropped and things might get forgotten).
It's also very much worth noting "don't pay for what you don't use" only refers to runtime cost. It doesn't say anything about developer implementation costs, or compile time costs. In fact, basically as a rule, every feature which runtime-free has non-negligible developer cost to use and compile time costs to optimize out.
The only example here really is implementing a += b;
as ptr::write(a, ptr::read(a) + b);
. This is fundamentally incompatible with a world where a + b
could unwind. Instead, this has to be implemented primitively or as ptr::write(a, { let bomb = AbortOnDrop; let a = ptr::read(a) + b; forget(bomb); a });
to temporarily force unwinds to abort the process.
(However, the presence of large types and the fact that move elision is spotty at best means that AddAssign
would still want to be a separate trait, even if default implemented. And it's just as easy to point the default in the other direction, without any unwinding issues; a + b
can be implemented as { a += b; a }
just fine.)
I just also want to point out one more time that in C, every operation that panics in Rust is "just don't do that, it's UB" in C. In specific situations, it might be easier to accidentally panic in Rust or easier to accidentally execute UB in C, but this is a property of library design, not of the language. There are huge parts of libc which go unused due to safety issues or just that they're unsuitable to your problem, and even huger tracts of the C++ STL go unused. While Rust isn't perfect for a must-not-unwind environment (which might just be a kernel? since anything that isn't the unwind implementation itself can probably afford unwinding support for the extra reliability?) it's clearly at worst as-good-as C or C++ in these environments, just younger, so there's less knowledge to go around of language-specific best practices for writing such code.
If a library causes an operation that previously was valid to now cause UB panic, then that is either a) a clear bug in the library (which, I may note, is language independent), or b) a case where you did library UB, which is your fault in unsafe
.
In all cases, I would rather my program halt (hopefully with some sort of indication of why) than to execute UB and have some sort of unpredictable, broken behavior with minimal, if any, indication of why. Obviously, (hopefully a message plus) not taking down the whole program is better, either via a caught task side-channel unwind or main-channel error unwind
One last thing to point out: you can't have subscript syntax without them having side-channel failure. A key feature of subscripting is that a subscript expression produces an lvalue in C/C++ terms, or a place in Rust terms. This is not a type you can wrap into an error handling reference type (easily... C++ has T&
lvalue references, and while you can't write std::optional<T&>
(and people complain about it), you do have std::reference_wapper<T>
).
The key problem is that arr[ix]
is a move and &arr[ix]
is a reference-to-place. Additionally, to add onto this, is that how you treat a place (by-move, by-ref, by-mut) is (in Rust) not a property of syntax, but inferred by how the place is used (via method autoref).
This is easily "resolvable" by just using methods to choose the behavior you want, rather than overloaded syntax. Perhaps it could even be served by an explicit return type overloading feature? But at the very least, it's a complicated problem space to address, without an existing solution.
On panic alternatives
I suspect that any such mechanism will look from a language level heavily like (potentially checked) unwinding. The fact that unwinding is implemented via OS unwinding is an implementation detail (kind of by definition in C++, but explicitly in Rust).
(Not-a-mod hat: yeah, pulling the how-to-avoid-panics talk into a different thread seems a good idea. If a mod splits it, sorry for mixing in this post...)