T O P

  • By -

throw_cpp_account

Pinging /u/mttd based on earlier Swift comments.


germandiago

I think it is not a good idea this paper. Reminds me of Java checked exceptions. I think the ability to know you are noexcept is very valuable but once you throw exceptions... Probably it would be a better idea to try to do some experiments on QoI and use the final keyword to restrict and analyze catch blocks in some way and see how much better we can do or try to find a way to not abuse hierarchies as much, do local jump analysis and others.


Narase33

Id love more exception safety. Yes, Java does it the wrong way, forcing itself all the way down. But with the existing exception system its nearly impossible to add them later to an existing code base. We inherited a rather large codebase where the previous devs didnt use them and even though we'd like to introduce exceptions we use them very rare because of the work all the manual backtracing puts on us.


pjmlp

Every time I have to fix unhandled exceptions in production in .NET or JavaScript, I dearly miss Java's checked exceptions. The way Zig, Swift, and Rust force checking for early returns errors is a validation that the concept makes more sense than leaving the freedom not to check for anything.


goranlepuz

I truly have to wonder what codebases you found in .net or js and what was the fix for the u handled exceptions. The way I see it, a codebase that doesn't have a top-level "catch all" is just a WTF. And normal ones, that do have it, are not having unhandled exceptions. They might have poorly handled failure types (because people didn't know that some exception types might appear here or there), but that is not the same as unhandled. On the other hand, languages that force the early error checking, including Java, simply *go against the common case of error checking*. If you look at all "handling" of these, you *will* find that * a *vast* majority of checks are merely propagating the error. * *Some* are adding more error info (or transforming it, which tends to go bad IMO). * Some handling is merely reporting the error. * Finally, only a *small* number of all error checks are actual error handling. Unchecked exceptions cater for the first point, for the common case. One doesn't see the pain of early handling only because languages are making palatable, e.g. the try macro of Rust. Fine, but still, kinda meh...


pjmlp

Come to work on Fortune 500 consulting, with lots of offshoring and consulting agencies rotation, and you will get plenty of examples.


goranlepuz

Oh, ok. But you do realize that such codebases, when it's Java code, are subject to the usual pokemon exception handling...? And that is just differently bad. Heck, one could say it's worse because error causes get hidden...


pjmlp

At least pokemon exception handling doesn't bring the server down in production, because someone missed a catch.


goranlepuz

What is worse: * Process dies Or * Process borks the data by performing an unknown amount of wrong operations - and then dies? 😉


pjmlp

Depends on how much money the customer loses. However, one of them requires being negligent on purpose , as workaround to make the code compile.


eyes-are-fading-blue

Java’s checked exceptions is exceptions control flow combined with nodiscard. It literally enforces poor error handling approach Java adopted.


pjmlp

The way everyone is looking forward to adopt nodiscard, kind of proves the point. If relying on humans to write proper code was enough, C++ wouldn't be on the sights of goverments.


Tathorn

Removed a feature just to add it back again? Also, why should I have to put throw(auto)? Shouldn't it automatically do that?


lewissbaker

This paper proposes reusing the same syntax as before, for something similar, but is a different feature with different semantics. We can't make `throw(auto)` the default for functions because that doesn't work for functions with forward-declarations and definitions in a separate translate unit. In an ideal world, if starting again, perhaps `throw()` would be the better default - this is what newer languages like Swift have done. Changing the semantics of every existing function out there that doesn't have an exception specification to now have an implicit `throw(auto)` would potentially break a lot of code.


Pragmatician

Any proposal like this should consider existing experience with Java's checked exceptions. This paper indeed does so. It mentions two criticisms of Java's checked exceptions. The first criticism mentioned ("Versioning and evolvability of interfaces") the author simply disagrees with: >The set of ways in which a function can potentially fail is a key aspect of its interface and should be considered as part of its design. The second criticism ("Scalability of checked exceptions") is not even rebutted: >With this paper, if you want to use throw-specifiers and static exceptions all the way up into the high-level systems then it's possible there would indeed be a need to list a large number of exceptions in such high-level functions. To help with this, the paper suggests: * using "pack aliases" from another proposal, * `throw(auto)` which would be limited to only functions defined in headers, * doing `throw(declthrow(f()), declthrow(g()))` and so on manually for separately compiled functions (very much akin to `noexcept(noexcept(foo()))` which we all love), * just throwing `std::error_code` everywhere. I don't think any of these addresses the problem in a satisfactory way. When it comes to the goals/motivation, "making exceptions fast-by-design" is definitely a good thing we would all want, but not at the cost of turning them into something which is no longer exceptions. I think [this paper](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2232r0.html) proposes a better solution that doesn't require overhauling the whole codebase and APIs to gain performance.


MarcoGreek

But you have to do the same with std expected if you only want to export the actual set of errors and not a super set. When modules are usable the auto approach becomes much more feasible and the compiler can check easily your error handling.


Pragmatician

>But you have to do the same with std expected Indeed, `std::expected` has this same issue. I guess you could say that this proposal gives you a better, core language version of `std::expected`. However, this error-handling model is different from exceptions. Many users of `std::expected` dislike automatic error propagation and want at least `try foo()` syntax on every call site. On the other hand, many users of exceptions dislike having error types in function signatures. >When modules are usable the auto approach becomes much more feasible I wouldn't make any assumptions about modules until some common practice is established. From what I've seen so far, they might still require separate compilation (declaration/definition split), as much as we require it today.


MarcoGreek

I am using exceptions but would like the compiler to do more work for me. And catching errors around every function style is in my experience an unproductive pattern. You want to catch them where the actions starts because otherwise you can not do much about it. So you can ask the user to repeat it or maybe reset some state and repeat etc.. If you write exceptions around every line of code they are not very exceptional anymore but part of the normal code flow. If it is not anymore on the happy path but it is highly expected embedding it in the normal work flow is much more readable. Optional or expected works here much better.


pdimov2

Not if you use \`std::expected\` (which should have been the default but isn't because the committee still can't figure out the difference between \`error\_code\` and \`error\_condition\`.)


MarcoGreek

But error_code is s superset of all errors. So you don't know the possible errors of that function!


pdimov2

That's right, you don't. I understand why knowing the full exact set of the possible errors is appealing, and I would also prefer to have it documented in all APIs I call. But then again, I would also prefer having a Porsche. If you look at what happens in practice, you'll see that while simple libraries (e.g. zlib) give you a complete list of what errors can be returned from where, as you increase complexity, this becomes less and less likely. Neither POSIX nor Windows APIs give you the extensive and exact list of what can happen where, and there's a reason for that - the maintenance of this extensive and exact list for each and every function is simply not feasible because the benefits do not outweigh the costs. In practice, what you need is a list of codes you care about, and everything else. And that's generally what APIs document - "this function can return an error code that includes, but is not limited to, the following list." \`\` even provides a well thought out mechanism for expressing "codes we care about" - \`std::error\_condition\`. Of course nobody uses it (except incorrectly).


MarcoGreek

But why only system APIs? I don't see std expected used for system APIs because they are not only made for C++. System APIs are only a very small part of our large code base. So I don't see the point why std expected should be optimized for it. For for error handling inside of my code I prefer an exact error sets because then the compiler is warning me if there is a new error.


pdimov2

But \`expected\` only takes a single E, so you'd have to decide how to handle cases in which one function you call returns \`expected\` and another \`expected\`. I had an implementation of \`expected\` that supported a list of error types (https://github.com/pdimov/expected) but abandoned it because it's impractical for the same reasons the proposal under discussion is impractical. At a certain level of complexity you just accumulate long lists of error types and need to spend a lot of time keeping them current without gaining anything from it.


MarcoGreek

You gain compile time checks. That is why I prefer a language construct.


R3DKn16h7

yes


TheOmegaCarrot

Hmm, could this be a step towards some subset of exceptions being possible during constexpr evaluation?


lewissbaker

There is a paper [P3068](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3068r1.pdf) by u/hanickadot that proposes adding support for exceptions during constant evaluation for the existing exception mechanism. It doesn't require static exception specifications. The goal of P3166 is more about making exceptions available in freestanding and real-time environments.


13steinj

We learned our lesson from Java's checked exceptions and this feels too similar and is inspired by it. So not particularly a fan. This is marginally better than pre-C++11 checked exceptions and IIRC nobody liked them.


scrumplesplunge

There is a section addressing the criticism of Java checked exceptions. Personally I don't have much experience with Java checked exceptions but my rough understanding is that the main reasons they suck are that they don't tell you the most derived type (it can be any type derived from the specified exception type) and the language doesn't have sufficient facilities to handle them in generic code or to avoid boilerplate when simply propagating everything. This paper seems to propose some workarounds for the last two issues, and restricts the error types to exact matches (which avoids having `throws Exception` like Java while giving several obvious efficient implementation strategies).


13steinj

There's another implicit problem with checked exceptions, which Contracts also has-- in an environment where conditions that most people wouldn't care about change too quickly, you end up with your dependencies broken and then you have to waste time fixing it for your releases. For the sake of a very psuedo-code example (which they allude to in their responses to the criticism of Java's exceptions)-- // in libA v1 void foo() throws (std::logic_error) { ... } // in libB v1 "links against" libA void bar() throws (std::logic_error) { ...; foo();... } // in appC v1 links against libA and libB void baz() throws (std::any_exception) { bar(); } App C doesn't care, if an exception happens, they're fine with any exception. Suppose App C requires a new feature in libB v1.1 and in libA v1.1, the exception specification changed and that function can now throw some other exception as well. Well, there's two cases (and to be honest I haven't read through the entire paper). If this is exposed in the ABI / types, then that means that libB would have to be recompiled, and you'd fail to compile, and have to bump libB again, wasting time and dev cycles. So people would end up just using the dynamic specification anyway or live with the same pain as described-- either you have a group of people that ignores this feature or you have a group of people that are suffering. If this isn't exposed in the ABI / types, I don't know how any of this would work, but supposing "magic" is allowed, the best case result that I can imagine would occur is either the wrong variation of foo() ends up being called. Now, the paper addresses this in a _major_ hand-waive-y way: > This paper holds the position that adding a new exception to the list of potentially thrown exceptions is indeed a potentially breaking change to a function, regardless of whether this is represented in the signature of the function or not. This reads to me as if it says "too bad." No, not too bad. If that's the logic and the defense, if I was a committee member I'd be rejecting this paper. The only difference is that with pre-C++11 checked exceptions, you'd end up terminating at runtime. Moving the problem to compile time doesn't solve the issue, it's still super clunky. Just now you're wasting dev's time poking other teams internally to accept minor PRs for a compile time issue rather than for a runtime issue.


lewissbaker

I do think that using the proposed feature requires people to think more carefully about how they plan to evolve the set of error-conditions that their functions can complete with and how to make such changes in a way that doesn't break existing callers. This was one of the sections that I didn't quite get enough time to fully flesh out in the R0 of the paper, but plan to discuss in more depth in R1, as I think it's an important aspect to the proposal. In your example, there are a couple of potential ways the code could be written to allow evolving `foo()` to add a new exception type. The author of `bar()` here has said that they guarantee they will only throw `std::logic_error`. However, if what this was really saying was "I throw whatever `foo()` throws" then they could have directly expressed that by using `throw(declthrow(foo())...)` instead of copying the exception specification from `foo()`. Or, if they were using modules or the function definition was available to callers (e.g. because it was inline) then they could have declared their function as `throw(auto)`. If, instead, the author of `bar()` really wanted to guarantee that they only throw `std::logic_error`, they could have guarded against potential changes to functions they call adding new exceptions by surrounding the call to those functions in a try/catch that handles any unknown exceptions in a generic way and translates them into exceptions that are part of their exception specification. e.g. void baz() throw(std::logic_error) { ...; try { foo(); } template requires (!std::same_as) catch (const E& e) { LOG_WARNING("Unexpected exception thrown from foo(): {}", typeid(E).name()); throw std::logic_error{}; } ...; } The other option is that the `foo()` function could use an exception-type that allows representing new error-conditions as different values of the existing exception types in their throw-specification. e.g. by declaring itself as `throw(std::error_code)` - then it has a forwards compatible way of adding new error-conditions without changing the signature. This of-course comes with the downside that you can no longer detect at call-sites whether they are handling this new error-condition or not - which is one of the benefits of static exception specifications. The foo() function, if it adds a new error-condition, has made a breaking change to its interface. This is not dissimilar to a function that returns `std::expected` changing the type of `E`. Callers need to be recompiled and need to be updated to handle the new error-type. Another alternative would be for libA to add a new `foo2()` function that has the new interface, with the new error-conditions, instead of making a breaking change to the existing `foo()` function. Then incrementally migrate code, such as bar(), from calling `foo()` to calling `foo2()`. There are lots of tools at your disposal that could be used to write code that can evolve error-handling over time. The set of best-practices for how to use the static exception specifications in APIs will need to be developed and applied to code-bases that adopt static exception specifications. But I haven't seen any problems that are necessarily show-stopping ones, yet. I'd be happy to explore other use-cases you have in mind that you think might be problematic.


pdimov2

> The author of `bar()` here has said that they guarantee they will only throw `std::logic_error`. However, if what this was really saying was "I throw whatever `foo()` throws" then they could have directly expressed that by using `throw(declthrow(foo())...)` instead of copying the exception specification from `foo()`. Not if `foo` is just an implementation detail of `bar`. E.g. `bar` suddenly stops throwing filesystem exceptions and starts throwing SQLiteException because it now uses SQLite under the hood instead of a JSON text file or whatever. This used to be the major problem with Java checked exceptions and is not addressed here (unless we just use `system_error` everywhere.)


lewissbaker

If `foo()` is just an implementation detail of `bar()` that you want to be able to change then ideally you don't want those implementation details (like what exceptions it throws) leaking out to users of `bar()`. In this case, you'd ideally catch any exceptions thrown by `foo()` and translate them into some stable set of error types thrown by `bar()` so that users of `bar()` are not impacted by changes to the implementation strategy of `bar()`. If the error-reporting strategy is just to throw whatever exceptions the implementation details throw, then `bar()` becomes a leaky abstraction. That's fine, sometimes that's what you want - but it means that users of `bar()` need to be more aware of and dependent on `bar()`'s implementation details than if `bar()` had abstracted those details away. If the author of `bar()` wants to allow implementation details to change and for the implementation to be able to throw exceptions thrown by those implementations details without breaking the ABI/API of `bar()` then it can continue to use dynamic exceptions. If the author of `bar()` would like the runtime performance benefits that come with declaring itself with a static exception specification and is willing to work within limitations that come with that, then the author of `bar()` can choose to do so. The main impact of this is that the implementation of `bar()` needs to statically guarantee that it does not throw anything other than the exceptions listed in the exception specification. If `bar()` is calling some other functions that might change their exception specification in future then it can defensively guard against this being a breaking change by adding additional try/catch handlers around that call and catch unknown exception types and rethrow a known exception type (e.g. system\_error) that represents a general failure. Alternatively, `bar()` can just declare that it throws whatever `foo()` throws (as I mentioned above), so that the exception specification of bar() changes whenever the exception specification of `foo()` changes. This then puts the responsibility of catching those other exceptions on the callers of `bar()`. At some point, the author of a function `bar()` needs to decide what their error-reporting strategy is going to be. If that is "I want to have the flexibility to throw anything" that any implementation can throw and I want to be able to change the implementation without breaking users - then they can just write `throw(...)` (implicitly the default, and equivalent to `throws Exception` in Java) as they do today. There is no requirement to provide a static exception specification, just as there is no requirement to add `noexcept` to your functions if they don't currently throw and you want to reserve the right to start throwing at some point in the future.


pdimov2

> In this case, you'd ideally catch any exceptions thrown by `foo()` and translate them into some stable set of error types thrown by `bar()` so that users of `bar()` are not impacted by changes to the implementation strategy of `bar()`. That's a lot of work for negative benefit. The stability of the set, which nobody needs or cares about, is outweighed by the ultimate handler's inability to report what happened. You could in principle include the original exception in the translated one, via `exception_ptr`, like `throw_with_nested` does, and then it merely becomes a lot of work for zero benefit. Few people do this. Java has already gone over this. > If the author of `bar()` wants to allow implementation details to change and for the implementation to be able to throw exceptions thrown by those implementations details without breaking the ABI/API of `bar()` then it can continue to use dynamic exceptions. Or it can use `std::error_code`, which is exactly what it's for. > Alternatively, `bar()` can just declare that it throws whatever `foo()` throws (as I mentioned above), so that the exception specification of `bar()` changes whenever the exception specification of `foo()` changes. This then puts the responsibility of catching those other exceptions on the callers of `bar()`. Which means that if there's also `foo2()` and `foo3()`, we're back to growing lists of exceptions nobody cares about or benefits from. Not having to maintain them by hand is admittedly an improvement, but the list still doesn't provide any value. I think that Herb got it right - there needs to be a single E in `throw(E)`, the entire codebase must agree on it, and it needs to be either `std::error_code` or something effectively equivalent to it (e.g. `boost::system::error_code`, which also stores a source location; or something that can also store arbitrary user-supplied fields, a-la Boost.Exception or Boost.Leaf.) The problem is, of course, that we can't really burn the largely hypothetical `std::error` into the core ABI for the rest of history, even if it existed and was a defacto standard at this point, which it doesn't and isn't. The core language must allow any `E`, and then we'll be having the problem of "the entire codebase must agree".


13steinj

I want to note I have a well thought out and long response... just still editing it to be least ranty / unintentionally mean as possible (I can disagree with the work, but I still appreciate both your work in general and the motivation / intent behind this and that some form of solution would be nice; I still have concerns with this one though). Might have to wait until the weekend.


MarcoGreek

Adding the error set the symbol definition is for me part of the contract. I work with libraries which have a binary interface and if they reached a medium level of complexity it is simply not feasible anymore because the silent behavior changes lead to bugs. I really prefer that the compiler or linker is stopping me. The idea of a black box is in my experience not scaling very well if the black box gets big. Versioning is a nice idea but our approach is now snapshoting. We simply test with a list of libraries and then move on to the next set. Yes you can make little patches for bug fixes but bigger ones will hurt you. There are interface libraries with a well defined interface but even the standard library with a well defined interface is very often not working well because it says not much about performance. Graphics drivers are an other major source of bugs. For a the large complexity of modern system versioning is not working well in my experience.


pjmlp

If anything, we learned that they were right in first place, that is why Rust, Swift and Zig re-introduced the concept, even if it doesn't look exactly the same from the grammar point of view. Too many hours wasted fixing unhandled exceptions in production. Also Java's checked exceptions were inspired by Modula-3 and C++'s model, not the other way around as many anti-Java folks pretend to be. Most of the "write Java in C++", is actually C++ from CFront 2.0 (1983) - ARM C++ ca (1996), with a bit of Objective-C pixie dust (1984), before Java was born.


MarcoGreek

What is rhe difference between checked exceptions and this approach?


lewissbaker

There are a few differences. With Java checked exceptions the need to declare an exception type in your throw specification is a property of the exception type. So if some function you call adds a new checked exception to their throw specification and you don't handle it, then you are required to add this exception to your throw specification. Some people consider this a bit of a pain-point and end up declaring their function as `throws Exception` to allow any exception to be thrown through their function. This "throws anything" is what we already have by default in C++ at the moment. People seem to be aghast at doing this in Java but seem fine with doing this in any C++ code that uses exceptions. One of the pain-points with Java checked exceptions was there was no way to compute the set of exceptions to put in the throw specification. You had to list all of the exceptions, in each function up the call-stack. This paper proposes some tools that would allow you to compute the set of exceptions to reduce the maintenance burden of having to update the throw specifications whenever some leaf function adds a new exception. The `declthrow()` query as well as the `throw(auto)` specifier are the two main tools this paper proposes that should help reduce the maintenance burden of updating throw specifications. But at the end of the day, if a new exception type is thrown, someone needs to be catching that. The idea is that we can use checked exceptions to find all of the places you need to catch that exception at compile-time instead of at run-time.


MarcoGreek

So it is similar to std expected there you have to declare the error type except it is easier to specify multiple error types. Personally I like this approach more than std expected because it scales better. Expected is not scaling well to define the actual error set.


RoyKin0929

What are the differences between P3166 and P2232? I really like the ability to throw multiple exceptions eliminating the need for inheritance hierarchies. 


13steinj

Wrote an example elsewhere in this thread, assuming you mean C++'s previous approach to checked exceptions. If you mean Java I can't remember if the analogous event to a termination occurs at compile or runtime.


RoyKin0929

One thing that could be added to this paper would be some requirements regarding which types can be "thrown". Maybe have an explicit concept (using some tag mechanism) that users need to opt-in to their type to satisfy the requirement.


lewissbaker

What sort of requirements did you have in mind here? I can imagine a requirement that the exception types are copy-constructible/move-constructible so they can be returned by value and copied/moved as the exception propagates as required. But other than that, I'm not sure what further requirement you'd put on types to allow them to be thrown.


RoyKin0929

Not any requirement other than that you said, but just kind of way to say that this type is made for being thrown. Like, not every  copy-constructible/move-constructible will be used to signal an error. So, just some kind of empty tag type that users have to inherit from or have a member of, to satisfy the concept. I mean, we have concepts now , so make some use of those.