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:
- A public
.has_value()
member function which returns abool
. - 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.
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)
{
}
};
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;
}
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
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));
… which outputs:
new_code(5) returns successful 5
new_code(0) returns failure argument out of domain