result< T, E >: Seamless error_code's with C++ Coroutines

The Idea

The other day I was reviewing some code when I saw the use of a macro to simplify handling an error_code like type. I’m sure most of us have seen code like this

// Either return the error or assign 
// the value type of rexpr to lhs.
#define ASSIGN_OR_RETURN(lhs, rexpr) ...

result<int, error_code> foo() { ... }

result<bool, error_code> bar() {
  ASSIGN_OR_RETURN(int i, foo());
  return i == 3;
}

In this example result<T,E> is a discriminated union / sum type, it will either hold the expected type T, or in case of an error, it will hold an E. The macro ASSIGN_OR_RETURN returns the error type if the error type is active and otherwise assigns the value type to i.

I think it’s pretty obvious why the use of the macro is less than desirable. Apparently the author of the code had picked this pattern up at google where result<T, error_code> = StatusOr<T>. While chatting they mentioned how they wish C++ had the Rust operator ? which effectively does the same thing as the macro.

But this got me thinking… Shortly before I was playing around with the new C++ coroutine TS (very excited ;). From my understanding, the coroutine keyword co_await effectively allows us to shim in arbitrary code at the site of the co_await operator. In particular, this mechanism can allow us to return early from the function. I was envisioning something like the following:

result<int, error_code> foo() { ... }

result<bool, error_code> bar() {
  int i = co_await foo();
  co_return i == 3;
}

While I am not the first to have this basic observation, e.g. boost::outcome, folly::expected, this reddit post, I beleive this capability deserves more attention and is a good example to learn from. Moreover, while exploring this idea I quickly discovered that this code be further simplified to just

result<int, error_code> foo() { ... }

result<bool, error_code> bar() {
  int i = foo();
  co_return i == 3;
}

All three of these example have the same exact behavior. If foo() returns an error_code then bar() will propegate this same error_code threw the return channel.

To my suprise this solution can also provide guaranteed exception safety. Consider the following,

result<bool, error_code> bar() noexcept {
  int i = foo();
  if(i == 2)
    throw std::exception();
  co_return i == 3;
}

At first glance, the noexcept seems incorrect. However, it is possible to configure the result<T,E> type so that it can capture all exceptions and record them as the error type, in this case error_core. In the implementation, I also show how to use std::exception_ptr as the error type to allow us to keep the runtime information contained in the current exception.

The result<T,E> type

The code I will be discussing can be found here but be warned it is just a prototype. For production-level code that does some of that I have described, see boost::outcome, folly::expected. My result<T,E> has the following form


template<typename T, typename E, typename ExceptionHandler>
class result
{
public:
  using value_type = T;
  using error_type = E;
  using exception_handler = ExceptionHandler;

  // constructors...
  result(const value_type& t);
  result(const error_type& e);
  result(const result& r);
  ...

  // accessing members 
  bool has_value() const;
  bool has_error() const;
  value_type& value();
  error_type& error();
  ...
  
  template <class Promise>
  struct awaiter {
    ...
  };

  struct promise_type {
    ...
  };
}

For now, we can ignore the exception_handler. The critical type for our concerns is the promise_type type. When the compiler sees one of the co_* keywords in a function body the compiler will look up the promise_type of the current return type, in our case result<T,E>::promise_type. The compiler will then roughly transform the function

result<bool, error_code> bar() {
  int i = co_await bar();
  co_return i == 3;
}

into

result<bool, error_code> bar() {
  using return_type = result<bool, error_code>;
  using promise_type = return_type::promise_type;
  promise_type prom;
  return_type ret = prom.get_return_object();
  
  result<int, error_code> b = bar();
  return_type::awaiter<...> a = promise_type::await_transform(b);
  if(a.await_ready() == false)
  {
    a.await_suspend(prom);
    return ret;
  }
  int i = a.await_resume();

  ret = i == 3;
  return ret;
}

As the implementer of result<T,E>, we have control over the body of these inserted functions. It is then a relatively simple matter to acheive the desired logic. First, we will have promise_type::get_return_object() -> promise_type& return a reference to itself and give result<T,E> the following constructor

template<...> class result {
  ...
  class promise_type {
    result* result_ptr = nullptr;
    ...
  };    

  result(proise_type& promise) {
    promise.result_ptr = this;
  }
};

The promise_type will then hold the pointer to the result as a member. The awaiter type a will then similarly hold a pointer b_ptr to b which is provided via the await_transform(b) function. During the call to a.await_ready(), the implementation will check if b contains an error_type and if so return false. The subsequant call to a.await_suspend(prom) can then set *prom.result_ptr = a.b_ptr->error(). Alternative, if b does not contain an error then a.await_resume() can similarly return the underlying value_type from b.

While this gives us a solid solution, we still must use the co_await keyword to generate this transformation. If you recall earlier, we desired to write

result<int, error_code> foo() { ... }

result<bool, error_code> bar() {
  int i = foo();
  co_return i == 3;
}

For this we will add an implicit converstion operator result<T,E>::operator T() which throws an E in the event that the error type is active. This convertion operator will then implicicitly be called for int i = bar();. In the event that an E is thrown we can imidiately catch it using the following

template<...> class result {
  ...
  class promise_type {
    void unhandled_exception() {
      try{ throw; }
      catch(error_type& e) {
        *result_ptr = e;
      }
    }
    ...
  };  

  result(proise_type& promise) {
    promise.result_ptr = this;
  }
};

The way this all works is as follows. When an exception is thrown in a coroutine it is automatically caught and within the catch block the functionpromise_type::unhandled_exception() is called. The current exception can then be rethrow using throw; which in turn allows us to catch the underlying error_type and store it in the return result<T,E>.

The final missing piece is how to handle arbitrary exceptions. For this, the library provides a way for the user to customize how an exception is converted into an error type. This is achieved by providing the third template parameter exception_handler with two member functions that contain the desired logic. When working with error codes one option is to simply throw out all the runtime information associated with the exception and store a generic error code indicating that an exception was caught and suppressed. This option can be found here.

Alternatively, it is possible to instantiate a result type with result<T, std::exception_ptr> where no runtime exception information is thrown away. It is then possible to specify an exception_handler which catches all exceptions and stores them as an std::exception_ptr, if the user does an unchecked access to the value_type, the captured std::exception_ptr can be rethrown using std::rethrow_exception(...). This option can be found here.

As a final remark, I’d like to say that this is just an example of what could be done. It is far from clear that everything I stated here is actually the best design choice. Certainly, the ideas about how to remove the use of the co_await keyword by throwing an exception are likely not the most efficient, although it is hard to say what the compiler will optimize away.

Cheers, Peter