outcome<T>, result<T> and option<T> all have formal empty states as
part of their programming model. They are quite useful for a multitude
of uses, I mainly have used them to early out from a sequence of
monadic operations or to signal an abort from a noexcept function.
Would you mind going into more detail with respect to the use cases?
That would be tricky. The empty state is for whatever the programmer
wants it to mean for some use case. It's a bit like returning a null
pointer, that could mean failure, or success, or something else. Totally
up to the programmer.
What does returning an empty result mean to the caller, conceptually?
The idea of these result types is to return either a value or a reason
for the lack of value, and an empty state is lacking both the value and
the explanation for why the value is missing.
The major refinement of outcome<T>, result<T> and option<T> over
expected is that you **never** get undefined behaviour when using
their observers like you do with expected. So when you call
.error() for example, you always get well defined semantics and behaviours.
This is intended to let you skip manually writing boilerplate when using
outcome<T>, result<T> and option<T>, and instead rely on the default
behaviours. Let's imagine this:
extern result<Foo> blah() noexcept;
...
result<void> nah()
{
auto v = blah();
if(v)
{
Do something with v.value() ...
}
else
{
Do something with v.error() ...
}
}
So if blah() returns success, we do something with the Foo returned, if
blah() returns failure we do something with the error returned. Those
are the two "normal" execution paths for binary success and failure. But
let's say blah() wanted to abort nah(), it returns an empty result. That
will test as boolean false, and when .error() is called you get a C++
exception thrown of type monad_error(no_state).
auto v = blah();
if(v)
{
Do something with v.value() ...
}
else
{
Do something with v.error() ... throws if v is empty
}
In the above case empty causes a C++ exception throw, which will abort
the caller's execution. You can however use that third state for any
meaning you like. It's the same difference as ternary logic over boolean
logic. If you don't want the empty state to cause a throw and simply be
a third state handled normally:
auto v = blah();
if(v.has_value())
{
Do something with v.value() ...
}
else if(v.has_error())
{
Do something with v.error() ...
}
else
{
result was empty ...
}
outcome<T>, result<T> and option<T> also have less-expressive to
more-expressive implicit conversion, so if you feed an option<T> to
something consuming an outcome<T>, it'll implicitly upconvert with no
loss of information, including loss of any empty state. In larger code
bases, it's very common to have low level code be using option<T> and
result<T>, and higher level code using outcome<T> or C++ exception
throws. The implicit convertibility without loss of original information
and avoiding needless boilerplate was a major design goal for Outcome,
as well as eliminating all the implicit reinterpret_cast<>'s which are
in std::optional<T> and std::expected.
I appreciate that all that was quite hand wavy, and I didn't give a
concrete use case. But I've used in my own code the empty state for
everything from an assert failure through to checking if a search found
something e.g.
outcome<Foo> found; // default constructs to empty
for(auto &i : container)
{
auto v = something(i); // returns a result<Foo>
if(v) // if not errored
{
found = std::move(v); // auto upconverts
break;
}
}
if(!found)
{
die();
}
It's really hard to be more specific. Do ask if the above doesn't make
sense to you. It's midnight as I type this, I am probably being unclear.
Niall
--
ned Productions Limited Consulting
http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/