Hacker News new | comments | ask | show | jobs | submit login
Zero-overhead deterministic exceptions: Throwing values [pdf] (open-std.org)
59 points by ingve 8 months ago | hide | past | web | favorite | 17 comments



I find it strange that this design forces you to use a single exception type, std::error, for everything you want to throw with the new mechanism. Even assuming it makes sense to use a single exception type throughout a given codebase, as opposed to using more specific per-function error types (which admittedly has drawbacks, as mentioned in the paper), I should still be able to define my own single type rather than using the STL's implementation.

Also, the idiom `throws(throws(foo))` feels like janky syntax. To explain, for anyone who hasn't bothered to read the proposal: you can use use `throws` in two different places with different meanings. Tacked onto a function declaration, `throws(COND)` means the function can throw depending on the value of COND, which should evaluate to a value of type `enum except_t`: one of no_except, static_except, or dynamic_except. But as an expression, `throws(EXPR)` does the reverse: it evaluates to an `enum except_t` representing whether/how evaluating `EXPR` can throw. So:

    template<typename Func>
    void foo(Func func) throws(throws(func()))
means that calling `foo(func)` can throw iff `func()` can throw. It makes sense, at some level… I just think it looks janky.


> I find it strange that this design forces you to use a single exception type, std::error, for everything you want to throw with the new mechanism.

I actually kind of like the idea. It ensures that error returns will be a fixed, small size, which enables the proposed ABI changes; and allows for attaching arbitrary information through std::exception_ptr, which avoids the need for checked exceptions' proliferation of error types.


The optimized ABI doesn’t depend on there being one error type for everything. Ideally it should be usable for any function parameter or return of a type that can handle being moved with memcpy - not just errors. And indeed, in the current Clang implementation you can stick [[trivial_abi]] on any class.


This paper suggests that people avoid C++ exceptions due to some problem with dynamic allocation or unpredictable cost or something. Maybe that's true in hard real-time circles, but in my day-to-day experience with C++ exception avoidance , I see very little rationality going into the decision.

I am continually frustrated by people who refuse to use C++ exceptions. These people claim to have a "philosophical objection" to the "unusable" exception model. They continually cite "bad" exception code involving try-catch clauses at every stack frame (which nobody actually writes), they claim that C++ exception handling produces "undefined" behavior (it doesn't), or that compiler support is somehow buggy (it isn't). These arguments make no sense. They have no persuasive power. They do not intersect with the real world.

Best of all, these very serious people, having written very sagacious emails banning random parts of C++ and having intoned that horrible things happen if one allows non-local flow control, switch from their email clients to their editors and go on to write Python and Java! Somehow, exceptions are cripplingly unusable in C++ yet not worth writing home about when used in Python or Java.

My theory is that most opposition to C++ exception usage is motivated mostly by cargo-culting and that most of the supposed arguments against exceptions are post-hoc rationalizations of a decision already made on the basis of non-technical factors. To wit: there were real performance and correctness problems with exceptions in early 1990s C++ compilers. These problems have been resolved for a very long time. Yet the 1990s meme that to be a responsible programmer, one should disable exceptions, or at best "use exceptions for exceptional situations" comes down to us in the present day. Too often, people blindly follow advice without seriously considering it, and it seems more likely that random bans on C++ exceptions demonstrate this phenomenon than any legitimate technical disagreement.


> they claim that C++ exception handling produces "undefined" behavior (it doesn't),

Exception safety (i.e. ensuring state is transactional) is hard in any language, but it's extra-hard in C++ where doing it incorrectly can lead to undefined behaviour rather than just an incorrect program. It's true that all of those sorts of problems with exceptions can occur just using normal `return`, but the hidden control flow of exceptions aggravates the problem by making it much harder to identify places of concern.

> Best of all, these very serious people, having written very sagacious emails banning random parts of C++ and intoning that horrible things happen if one allows non-local flow control, switch from their email clients to their editors and go on to write Python and Java! Somehow, exceptions are cripplingly unusable in C++ yet not worth writing home about when used in Python or Java.

This seems unreasonable. In general, people aren't nearly as demanding about things like performance for tasks they're solving with Python and Java as with C++. Additionally, I'm sure a significant chunk of those who dislike exceptions in C++ also dislike them elsewhere, but there's a variety of reasons that they're using any of those languages despite that (an inherited codebase, library support, ...).

> To wit: there were real performance and correctness problems with exceptions in early 1990s C++ compilers. These problems have been resolved for a very long time.

Clang, a C++ compiler, and LLVM, its optimizer back-end, don't use exceptions. While there are probably a variety of non-technical factors also coming into play, it seems hard to swallow that literal C++ compiler writers would entirely cargo-cult a major choice like this.


There's an element of Nissan Taleb's "Dictatorship of the Small Minority"[1] at work here.

If you bring together a random group of C++ programmers, some of them will approve of exceptions and some of them will see exceptions as an abomination. The former group will grudgingly write non-exceptional code; the latter group will not write exceptional code (at least in C++). Consequently, the codebase ends up banning exceptions, the fact of which the anti-exception people use as evidence for more exception bans in other projects.

This dynamic doesn't speak to the validity of the argument against exceptions. It just indicates that it's usually easiest, socially, to just appease the anti-exception people.

[1] https://medium.com/incerto/the-most-intolerant-wins-the-dict...


In this case though, the dynamic goes both ways. Programmers brought up on modern C++ will frequently be just as intransigent against error-return values and other alternatives to exceptions.

In my own experience, exceptions are useful because real programmers can't be trusted to handle errors at all. So it is better to crash the program than to allow silent corruption. And exceptions are more politically palatable than assertions.

But if the quality of your team AND processes are high enough you can expect errors will actually be handled, then it is easier to do that correctly without exceptions.


I hope that reasonable people in both camps can agree that blatant contract violation and assertion failure should result in fail-fast instant death. It's a sad environment that bans assertions.


This is a step in the right direction. C++ exceptions in practice are difficult to get right, and depending on dynamic allocation in an exception seems problematic to me.

One of the main reasons to use exceptions is to save code. In one of my projects, using exceptions instead of checking error returns saved 11% in code size. The exception version of the code was more correct, since there were many fewer places where error codes were handled. The golang people just don't get this.

The subtleties are not longer fresh in my mind, but "throws" rubs me the wrong way. Why declare whether a certain function may throw an exception? The beauty of exceptions is that you can use code that throws exceptions without having to know exactly where exceptions can occur.


> The subtleties are not longer fresh in my mind, but "throws" rubs me the wrong way. Why declare whether a certain function may throw an exception? The beauty of exceptions is that you can use code that throws exceptions without having to know exactly where exceptions can occur.

Throwing an exception does not alter the ABI for a function, since the mechanism for throwing exceptions is separate from normal control flow. They call into library code for unwinding the stack. This is what makes them "slow", and it's why they require heap allocation since there's no pre-made place to store the value.

Throwing a value (as proposed) does alter the ABI for a function. That value needs a place to go, it's essentially another return value. Removing the requirement to add throws to the declaration would be like allowing these two functions to be the same:

    int foo();
    std::either<int, std::std_error> foo();


Why not eschew the idea of "exceptions" at all then, and just return an error outcome if it can be expected? You've just wrote down a way to do that with no ABI changes.

A more expensive (and preferably non-allocating) mechanism could be used for a controlled "panic", that is, winding things down in an orderly manner when an unpredictable error occurs.


It seems a bit like the difference between

    async fn foo() -> int
and

    fn foo() -> Async<int>
(pseudo-rust syntax but the distinction also applies to C# and JS, et. al.)

Async code and error handing can both be implented with regular code, but there are cases where some compiler magic can be more elegant, more expressive, or more performant. In these cases it's nice to have a construct that distinguishes this magic from regular values, rather than just making a specific return type magic, IMO. In the throwing values proposal, the magic is to make a regular value usable in exceptions, and also to use mostly-hidden CPU state (flags register) to indicate the type of the value. This allows the C++ proposal to use less memory and perhaps be more performant than rust-style error values. (This ABI change could also be used in other languages like Rust, as mentioned at the end of the document)


good error/failure framework:

- should be obvious which functions can fail and what errors they can fail with

- propogating an error up the stack should be visible in the code and semantically easy to do

- errors should be composable and easy to add additional information to

- deciding to not handle a error should be explicit and otherwise produce a warning

- errors should be fast and cheap to return errors

- (maybe nice to have) ability for a function to have and return multiple errors


As someone who's not into real-time or zero-overhead, this is still interesting:

Is it feasible to restrict exceptions to throwing a single simple "error code" type without losing much of value?


It still works fairly well for knowing whether something can be recovered from, but you do lose some context, which needs to be provided in other ways for things like debugging.


I wonder if it wouldn't be possible to have 'heisenberg exceptions': if an exception is caught, it's just a normal exception, but if it isn't caught then there is a core dump which means you get all the data you need to debug the issue.


This is very similar to how go's panic/recover work. In Java an uncaught exception will kill just the thread (and usually log the stack trace in the process).




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: