-
Notifications
You must be signed in to change notification settings - Fork 257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SUGGESTION] Value based exceptions (Zero-cost exceptions) #111
Comments
To add to this, it could be implemented via |
Completely agree, |
I do not think we want this behavior. The kinds of errors a function can throw should be considered part of its contract. So if we go with the current example every time the In the case of functions provided by external dynamic libraries this is actually the only way to know which error values can even be thrown as the function body may not be available at compile time. They may not be here in cpp2 now but have to be considered for this feature. |
I think I remember a speaker mentioning otherwise, just like C++1 moved away from |
Rust essentially makes exactly this with its |
Right, it remains in the signature. I think I got confused with how |
Exactly. |
Original run: (file: &std::string) -> int32_t throws {
file := File::open(file).try;
contents := std::string{};
file.read_to_string(contents&).try;
return contents.trim().parse().try;
} compiler could deduce that |
I do not think it is good to allow for this deduction by default for the reasons I have given in this comment. However it would be possible to use this deduction logic to offer a tool that generates the |
Specifically about listing/deducing the exceptions that can be thrown: @redradist Here's one brain dump of the issues: https://herbsutter.com/2007/01/24/questions-about-exception-specifications/ Probably the most fundamental issue is that listing a specific set of exceptions is not composable... here are two main aspects of that:
Note that (1) is still true even if the specification is deduced by the compiler (which also only works if you have the full source code for the entire call tree, which we won't always have and is another reason it's not composable). AIUI the languages that chose to list exceptions have either backed off and deprecated/removed that feature or else have had their users de facto disable it in practice (e.g., Java |
FYI, @redradist, the proposal linked at https://github.com/hsutter/cppfront#2019-zero-overhead-deterministic-exceptions-throwing-values, IIRC, allows for value-based exceptions of arbitrary load with a single type. |
I partially agree with that, but in Java the issue was that list of exceptions should be updated manually.
No, it is possible if list of exceptions would be a part of signature
Okay, one additional idea, main: void = {
run("Example string");
}
run: (file: &std::string) -> int32_t throws {
file := File::open(file).try;
contents := std::string{};
file.read_to_string(contents&).try;
return contents.trim().parse().try;
} will be converted to: main: void = {
error_buf: std::array<uint8_t, 2035>= {};
run("Example string", error_buf);
}
run: (file: &std::string, error_buf: &std::array<uint8_t, 2035>) -> int32_t {
file := File::open(file).try;
contents := std::string{};
file.read_to_string(contents&).try;
return contents.trim().parse().try;
} An error would be stored in |
This will not work with run: (file: &std::string) -> int32_t throws Error1, Error2; becomes run: (file: &std::string) -> int32_t throws Error2, Error1; This function would become
This does not solve the fundamental issue of this approach as you still need information about the exception types for a function. Again, if the source code is not available, you can not perform this analysis. Even if it is you need to re-compile your code if somewhere in the call chain an exception type changes. For example if deep in the call tree of |
Going back to send_string: (in s: std::string) -> std::expected<size_t, send_error<std::string>>; In case |
To me, using As for multiple alternative error types, you can use IMO it should not be a fully generic system that can propagate any kind of error implicitly, because then it is no better than the classic exceptions (and will require the runtime generic object handling overhead too).
I agree, after all the purpose of |
I have checked the impact switching to this kind of error handling as the default would have on the Error Handling part of the C++ core guidelines
Based on this error handling strategy more changes could be implemented.
I expect the first point to require the most amount of discussion and changes to make the idea work. |
Maybe a less informed user-point-of-view: If the compiler deduced what kinds of exceptions a given function may throw, it could warn about exceptions that have not been handled. In terms of calling functions whose exceptions specs are not known, cpp2 could assume that the user writing the code knows the possible exceptions and give them a way to explicitly handle those (with shortcuts for "ignore" and "abort if encountered") and if anything passes through this handling we'll abort (essentially wrapping the unknown call in a noexcept labelled wrapper function that also contains some error handling for that function) |
I disagree with this. If I change my implementation to call a different function that has a different exception specification, then it's my responsibility to make sure I don't propagate them if my callers don't expect arbitrary unspecified errors. This is true whether the language helps me here or not. Consider e.g. that same situation with error codes: /** Returns 0 on success, -1 if the directory already exists. */
int CreateDirectory(std::string name) {
// ... do things
int error = CallNewFunction(); // returns -2 if a network connection can't be established
if (error) { return error; }
// ... do things
} I have exposed implementation details and broken my callers. The only difference is the language didn't help me prevent it. The virality of it happens if I decide the solution is to add "can also return -2" to the documentation; which is the wrong thing to do here. And if propagating is the right thing to do, the language is not helping my callers by not signaling the breaking change: They have to stumble upon it in human language. But error specification (of the type "expected situation outside the caller's control") is part of my function interface, whether I describe them in C++ or in English. Of course, sometimes we're writing a function that's not part of any module's interface, e.g. just some internal wrapper around another function call. Similarly to how in that case I might want to specify
Same deal, no? What return type do I write for The parallelism between return types |
One of the first ideas in this issue was to use
This is the viral part of this exception list approach. Once you call one function with a list specified with |
I think Boost.LEAF can achieve what's desired here (that is, arbitrary errors from the callee which the caller has to handle):
|
I don't think that is true. You could handle all errors and only let specific errors pass. E.g. try {
func_with_auto_throws_decl();
} catch (const std::invalid_argument &) {
throw;
} catch (...) {
std::abort();
} which would only let errors of type |
@Krzmbrzl |
Yes, that I absolutely agree with. Though, the mental burden would not really be that big as the compiler could (should!) tell you explicitly which exceptions you have not yet handled yet, so then you can only handle those explicitly. |
Thanks for all the comments, everyone. Yes, the intent is for Cpp2 to use value-based exceptions, throwing by value using a single type (some two-word type similar to
I understand, but let's agree to disagree. I don't think that's desirable for the reasons I gave above (exposes implementation detail, brittle, not composable, not generic), and I'll add one thing: A major point of error/exception handling is that it's desirable to separate the error handling code from normal control flow (which makes the normal control flow clearer), and not only is the handler typically further up the call tree than the immediate call site (i.e., the call site often doesn't know what to do with the error), but it has been commented that "the value of the exception increases with distance thrown" as the distant higher code has more context and this is where automatic propagation really shines (as opposed to having to manually propagate It's still fine for a function to return an explicit status code for an immediate caller, especially for common situations (which IMO aren't "errors" -- an "error" should mean that the function could not accomplish what it promised, which does not include success-with-info). Real errors (whether delivered by codes or exceptions) are typically not for the immediate call site to handle -- typically the closest they're handled is in a batch later in the calling function (see C's That's why IMHO it's a major weakness of Thanks! |
@hsutter and all other, Let me add a few points why I think returning
void main() {
run("Example string");
}
int32_t run(const std::string& file) throws {
auto file = File::open(file);
const auto& contents = std::string{};
file.read_to_string(&contents);
return contents.trim().parse();
} and instead of dynamic allocated memory on heap, compiler knows exception flow due to void main() {
std::array<uint8_t, 2035> exception_buffer = {};
run("Example string", error_buf);
}
int32_t run(const std::string& file, std::array<uint8_t, 2035>& exception_buffer) {
auto file = File::open(file);
const auto& contents = std::string{};
file.read_to_string(&contents);
return contents.trim().parse();
}
run: (file: &std::string) -> int32_t throws Error1, Error2 {
file := File::open(file).try;
contents := std::string{};
file.read_to_string(contents&).try;
return contents.trim().parse().try;
} or run: (file: &std::string) -> int32_t throws {
file := File::open(file).try;
contents := std::string{};
file.read_to_string(contents&).try;
return contents.trim().parse().try;
} The compiler could in first case mangle the function in the following way I believe the list of exceptions, a visible exception flow and value semantic could increase Cpp2 "safety", performance and determinism by a lot !! |
I believe that part of the discussion here, might be better covered in a suggestion of itself, that does not require compiler enforcement. I have created #144 as an attempt to factor out the discussion focused on code readability and toolability from this suggestion, which seems to be mainly concerned about finding a more performant way of actually implementing exceptions themselves. |
This feature is based on https://youtu.be/ARYP83yNAWk?t=2227
Long time ago I wrote to you an email about it, but it was lost somewhere in history of civilisation ...
Anyway, I just want to extend your idea to use value based exceptions like it is done in Rust for example with
Result<,>
class and?
syntaxSee the code:
This function could throw 2 exceptions Error1 or Error2.
Implementation could be done using
union
and value for describing type of error.See godbolt possible underlying implementation: https://godbolt.org/z/a_vbNw
I suggest to convert all functions without
throws
keyword ( in cpp2 syntax) to function withnoexcept
(in C++ current syntax)Also a list of exceptions could be generated automatically by analyzing which new cpp2 syntax functions is called from current context:
could be converted to:
Because cpp2 compiler knows that , for example,
File::open(file).try
andfile.read_to_string(contents&).try
could throwError1
orError2
The text was updated successfully, but these errors were encountered: