Extending OUTCOME_TRY

Outcome’s OUTCOME_TRY(var, expr) operation is fully extensible to accept as input any foreign types. It already recognises types matching the concepts::value_or_error<T, E> concept, which is to say all types which have:

This should automatically handle inputs of std::expected<T, E>, and many others, including intermixing Boost.Outcome and standalone Outcome within the same translation unit.

OUTCOME_TRY has the following free function customisation points:

OUTCOME_V2_NAMESPACE::try_operation_has_value(X)
Returns a `bool` which is true if the input to TRY has a value.
OUTCOME_V2_NAMESPACE::try_operation_return_as(X)
Returns a suitable failure_type<EC, EP = void> which is returned immediately to cause stack unwind. Ought to preserve rvalue semantics (i.e. if passed an rvalue, move the error state into the failure type).
OUTCOME_V2_NAMESPACE::try_operation_extract_value(X)
Extracts a value type from the input for the `TRY` to set its variable. Ought to preserve rvalue semantics (i.e. if passed an rvalue, move the value).

New overloads of these to support additional input types must be injected into the OUTCOME_V2_NAMESPACE namespace before the compiler parses the relevant OUTCOME_TRY in order to be found. This is called ‘early binding’ in the two phase name lookup model in C++. This was chosen over ‘late binding’, where an OUTCOME_TRY in a templated piece of code could look up overloads introduced after parsing the template containing the OUTCOME_TRY, because it has much lower impact on build times, as binding is done once at the point of parse, instead of on every occasion at the point of instantiation. If you are careful to ensure that you inject the overloads which you need early in the parse of the translation unit, all will be well.

Let us work through an applied example.


A very foreign pseudo-Expected type

This is a paraphrase of a poorly written pseudo-Expected type which I once encountered in the production codebase of a large multinational. Lots of the code was already using it, and it was weird enough that it couldn’t be swapped out for something better easily.

enum Errc
{
  kBadValue
};
template <class T, class E = Errc> struct ForeignExpected
{
  T Value;
  E Error;
  int IsErrored;

  ForeignExpected(T v)
      : Value(v)
      , Error()
      , IsErrored(0)
  {
  }
  ForeignExpected(E e)
      : Value()
      , Error(e)
      , IsErrored(1)
  {
  }
};
View this code on Github

What we would like is for new code to be written using Outcome, but be able to transparently call old code, like this:

ForeignExpected<int> old_code(int a)  // old code
{
  if(0 == a)
    return kBadValue;
  return a;
}

outcome::result<int> new_code(int a)  // new code
{
  OUTCOME_TRY(auto x, old_code(a));
  return x;
}
View this code on Github

Telling Outcome about this weird foreign Expected is straightforward:

OUTCOME_V2_NAMESPACE_BEGIN
template <class T, class E>  //
inline bool try_operation_has_value(const ForeignExpected<T, E> &v)
{
  return 0 == v.IsErrored;
}
template <class T, class E>  //
inline auto try_operation_return_as(const ForeignExpected<T, E> &v)
{
  switch(v.Error)
  {
  case kBadValue:
    return failure(make_error_code(std::errc::argument_out_of_domain));
  }
  abort();
}
template <class T, class E>  //
inline auto try_operation_extract_value(const ForeignExpected<T, E> &v)
{
  return v.Value;
}
OUTCOME_V2_NAMESPACE_END
View this code on Github

And now OUTCOME_TRY works exactly as expected:

  auto printresult = [](const char *desc, auto x) {
    if(x)
    {
      std::cout << desc << " returns successful " << x.value() << std::endl;
    }
    else
    {
      std::cout << desc << " returns failure " << x.error().message() << std::endl;
    }
  };
  printresult("\nnew_code(5)", new_code(5));
  printresult("\nnew_code(0)", new_code(0));
View this code on Github

… which outputs:

new_code(5) returns successful 5

new_code(0) returns failure argument out of domain