Extending BOOST_OUTCOME_TRY - Boost.Outcome documentation

# Extending `BOOST_OUTCOME_TRY`

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

- A public `.has_value()` member function which returns a `bool`.
- In order of preference, a public `.assume_value()`/`.value()` member function.
- In order of preference, a public `.as_failure()`/`.assume_error()`/`.error()` member function.

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.

`BOOST_OUTCOME_TRY` has the following free function customisation points:

`BOOST_OUTCOME_V2_NAMESPACE::``try_operation_has_value(X)`
  Returns a `bool` which is true if the input to TRY has a value.
`BOOST_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).
`BOOST_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 `BOOST_OUTCOME_V2_NAMESPACE` namespace before the compiler parses the relevant `BOOST_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 `BOOST_OUTCOME_TRY` in a templated piece of code could look up overloads introduced after parsing the template containing the `BOOST_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
    {
      BOOST_OUTCOME_TRY(x, old_code(a));
      return x;
    }

View this code on Github

Telling Outcome about this weird foreign Expected is straightforward:

    BOOST_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;
    }
    BOOST_OUTCOME_V2_NAMESPACE_END

View this code on Github

And now `BOOST_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

Last revised: June 22, 2019 at 21:22:42 +0100