7

I've been reading some articles on how Rust does error handling using the Result<T, E> type and to me it seems like a hybrid best-of-both-worlds (exceptions and return codes) solution which can be very useful. I think it would especially be handy for APIs where you don't really know on beforehand if certain errors are exceptional and should throw, or that they are merely an indication of something going wrong in an 'expected' way. Returning Result allows moving that decision to the users of the API in a not too intrusive way. So I'm now debating whether or not to introduce this in a C++ project. The result class could be implemented roughly like this:

template< class T, class E = std::exception >
class result
{
public:
  //constructors etc

  operator bool() const
  {
    return !!res;
  }

  T& operator * ( )
  {
    if( !res )
    {
      assert( ex );
      throw *ex;
    }
    return *res;
  }

  const E& exception() const
  {
    assert( ex );
    return *ex;
  }

private:
  std::optional< T > res;
  std::optional< E > ex;
};

So whene there is a function

result< int > Read();

callers can use either 'error-code' style:

const auto result = Read();
if( !result )
{
  log.debug() << "failed read" << result.exception();
  return false; //or again return a Result
}
doSomething( *result );

or 'exception' style:

try
{
  doSomething( *Read() );
}
catch( const std::Exception& e )
{
  //do what's needed
}

or decide they don't know how to handle exceptions from Read and let their caller in turn deal with it

doSomething( *Read() ); //throws if !result

Does this seem like a good idea in general, and are there similar principles used in existing C++ code? Or are there serious disadvantages I'm not seeing at the moment? Like is it too complicated towards the users because there are too many options or maybe because it is not a typical system used in C++?

4

There are major differences between throwing exceptions and returning error objects, in particular,

  • exceptions create many new code-flow paths which are not explicitly visible in the source code,
  • callers have no idea what kinds of errors they can expect,
  • they propagate automatically, and
  • they make the code much less verbose and easier to read and write.

Rust tries to get the best of both worlds by banning exceptions (this eliminates the first two points) and by having the try! macro (this deals with automatic propagation and verbosity).

Your solution deals with the first two points only and there is no straightforward way to have the latter two. Your * operator is not an equivalent of try!. The equivalent of

x = try!(foo());

is simply not possible to do in C++ (without language extensions).

That said, feel free to use your class, though as @Deduplicator points out, your Result class can only hold exception objects of dynamic type std::exception, which is probably not what you intended. The easiest way to accomplish what you want is to use std::exception_ptr to hold the exception object.

Or, better yet, just use exceptions -- that's what the language was designed for after all.

  • 1
    The idiomatic way today is to use let x = foo()?; and not try!. The macro has been deprecated. – Centril Apr 9 '18 at 9:52
1

Well, it could conceivably be useful. If the function shouldn't have used exceptions anyway.

But your type is all wrong:

  1. You have to allocate the exception dynamically (or at least allow for it being allocated dynamically) to allow for returning a sub-type.
  2. Avoid stacking mutually exclusive optionals on top of each other, that just wastes space. And stack-space is actually somewhat valuable. Take a look at Boost.Variant.

If you do correct #2, you might be able to do a small-exception-optimization for #1 and thus avoid dynamic allocation most of the time.

  • Well, this is more a review of the c++ implementation but thanks anyway. I'm not sure I completely understand point 1. Do you mean using something like std::unique_ptr< E >? – stijn Jul 13 '15 at 14:04
  • Yes, that's a possibility. Though better take a look at how Boost.Variant is implemented, and start from there, for small-object-optimization (avoiding dynamic allocation) and to also incorporate point 2. – Deduplicator Jul 13 '15 at 14:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.