Hybrid result/try/catch vs a new direction in C++


#1

Recently there was much debate about adding try/catch-like mechanisms to Rust (but without actual exceptions). Whereas that was starting with return values and considering if there was value in taking a step towards exception-style behaviour, yesterday I stumbled upon the following C++ working group paper which recommends that C++ take a step towards return value-like behavior and ALSO proposes a hybrid approach to error handling. It’s written by Herb Sutter, who approaches Stroustrup’s stature in the C++ community, and he does a good job of explaining the problems with exceptions that have led to them not being adopted in C++ code written by Google, F-35 fight software, Windows kernel, etc.

It’s an extremely interesting read and if you squint a bit not so different from recent Rust discussions. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf


#2

For reference, there was some previous discussion here: https://www.reddit.com/r/rust/comments/8iwm5x/c_throwing_values_proposal_resultlike_error/

There’s also a proposal in progress to add a “failure return” feature to C and its ABI, which this could desugar to for interop purposes, and which Rust Result could take advantage of: https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/HV-7y5XZmBw


#3

I find the chart in section 3.1 to be particularly interesting. Something like this should be created for Rust and made part of the Nomicon. A chart that lays out the explicit goals of the Rust error handling could be helpful to keep things/proposals reasonable for the goals of Rust.


#4

Also for reference, see Boost Outcome, which has been accepted into Boost as of 1.69. It works on C++14 without any langauge changes. It’s basically as close to Rust’s Result as is possible without language level changes.

auto get_int_from_file(string_view path) noexcept
-> outcome::result<int>
{
  OUTCOME_TRY(str, (read_data_from_file(path)));
  // if control gets here read_data_from_file() has succeeded
  return process(str);  // decltype(str) == string
}

I was initially excited about the prospect of using Rust-style return-oriented error handling, but my enthusiasm has been dampened as I’ve dug more into it, to the point where I am sad to report I’ll probably stick to exceptions for my typical “modern” C++ projects. :slightly_frowning_face:

What Outcome doesn’t have:

  • The convenient, short, ? operator
  • match
  • Compile time guarantees you’ve upwrapped things correctly

You will probably find this surprising. This is because the default action for a user-defined error type is undefined behaviour

I really don’t have space in my brain for one more possible way to add UB to my C++ projects.

All of this lead me to believe Rust did a really fabulous job on how it does error handling, you really need all three of these features to make return-oriented error handling worth the overhead over the conveniences of happy-path-optimized exception handling.


Syntax highlighting other languages?
#5

The paper linked to by @ibkevg is really insighfull. Outcome is not what this paper suggests. The paper suggests having functions marked as “throws” and then having throw types be unioned with return types on those throwable functions.

The compiler would then associate some sort of flag (either by register or CPU bit flag) with a thrown value, so that you know whether or not your type was actually returned or you got an error. The error information could then be automatically be propagated with lowest overhead, and no unwinding providing the benefits of outcome expected, errorcodes and exceptions with no added overhead (and reduced overhead possibly due to sharing return channel). This would be a language feature to C++.


#6

Oh yeah, sorry wasn’t trying to imply that. Just wanted to toss out Outcome as “something else” happening in the C++ world of error handling.


#7

IMHO, Rust could really land itself in this “best of both worlds” sweet spot too, for example with some of the try/catch stuff discussed in recent months. Code in the try block might automatically behave as though ? was written after every function call that returns a result, and the catch block could be like a match on errors, letting you handle the error return codes out of band from the success path.

There’s definitely some peanut butter than could be mixed with the chocolate here.


#8

That sounds like a really nice idea


#9

yeah, but I think having an explicit mechanism for this is probably preferable (like how the paper proposes throws after function definition) there is always going to be some cost for this, and having it be explicitly would give people the choice if they need it (return type may be smaller than error type, thus more registers would be used than otherwise, using extra register/flag, multi threaded exceptions etc…)


#10

Well I think C++ has/requires exceptions (especially for constructor and operator failures), has the clean success path code story they offer but what they needed was a way to get value return-like performance and determinism. Whereas, I think Rust is the opposite, has the efficient value return errors but doesn’t quite have the clean success path code story yet that exceptions offer.


#11

I’m confused why that means what you propose shouldn’t be explicit.


#12

To me, the lesson here is that exceptions and value return are not 100% orthogonal as they are often made out to be and there is a hybrid between the two that is possible.

The C++ solution described in the paper, makes the sense for it given that it’s starting point is exceptions and they want to take steps towards value return, whereas I think Rust has a great value return story through error and failure crate-like techniques and doesn’t need the throws channel talked about for C++. Rust can benefit from machinery traditionally associated with exceptions such as grouped try/catch mechanisms. Rust’s ? is great if you want to return immediately but less great if you have some error handling behaviour to need to do within your function as catch would allow. Further, what if a try without a following catch would automatically return out of the function, like a short hand for ? after everything?