So I ran across this (IMHO) very nice idea of using a composite structure of a return value and an exception - Expected<T>
. It overcomes many shortcomings of the traditional methods of error handling (exceptions, error codes).
See the Andrei Alexandrescu's talk (Systematic Error Handling in C++) and its slides.
The exceptions and error codes have basically the same usage scenarios with functions that return something and the ones that don't. Expected<T>
, on the other hand, seems to be targeted only at functions that return values.
So, my questions are:
Expected<T>
in practice?Update:
I guess I should clarify my question. The Expected<void>
specialization makes sense, but I'm more interested in how it would be used - the consistent usage idiom. The implementation itself is secondary (and easy).
For example, Alexandrescu gives this example (a bit edited):
string s = readline(); auto x = parseInt(s).get(); // throw on error auto y = parseInt(s); // won’t throw if (!y.valid()) { // ... }
This code is "clean" in a way that it just flows naturally. We need the value - we get it. However, with expected<void>
one would have to capture the returned variable and perform some operation on it (like .throwIfError()
or something), which is not as elegant. And obviously, .get()
doesn't make sense with void.
So, what would your code look like if you had another function, say toUpper(s)
, which modifies the string in-place and has no return value?
Void functions are created and used just like value-returning functions except they do not return a value after the function executes. In lieu of a data type, void functions use the keyword "void." A void function performs a task, and then control returns back to the caller--but, it does not return a value.
In computer programming, when void is used as a function return type, it indicates that the function does not return a value. When void appears in a pointer declaration, it specifies that the pointer is universal.
A void function cannot return any values. But we can use the return statement. It indicates that the function is terminated. It increases the readability of code.
In contrast, std::expected can indicate an expected value and an error value, which is equivalent to the two-member std::variant , but is more convenient to use on the interface. Think of it as a new kind of error handling.
Have any of you tried Expected; in practice?
It's quite natural, I used it even before I saw this talk.
How would you apply this idiom to functions returning nothing (that is, void functions)?
The form presented in the slides has some subtle implications:
This does not hold if you have expected<void>
, because since nobody is interested in the void
value the exception is always ignored. I would force this as I would force reading from expected<T>
in Alexandrescus class, with assertions and an explicit suppress
member function. Rethrowing the exception from the destructor is not allowed for good reasons, so it has to be done with assertions.
template <typename T> struct expected; #ifdef NDEBUG // no asserts template <> class expected<void> { std::exception_ptr spam; public: template <typename E> expected(E const& e) : spam(std::make_exception_ptr(e)) {} expected(expected&& o) : spam(std::move(o.spam)) {} expected() : spam() {} bool valid() const { return !spam; } void get() const { if (!valid()) std::rethrow_exception(spam); } void suppress() {} }; #else // with asserts, check if return value is checked // if all assertions do succeed, the other code is also correct // note: do NOT write "assert(expected.valid());" template <> class expected<void> { std::exception_ptr spam; mutable std::atomic_bool read; // threadsafe public: template <typename E> expected(E const& e) : spam(std::make_exception_ptr(e)), read(false) {} expected(expected&& o) : spam(std::move(o.spam)), read(o.read.load()) {} expected() : spam(), read(false) {} bool valid() const { read=true; return !spam; } void get() const { if (!valid()) std::rethrow_exception(spam); } void suppress() { read=true; } ~expected() { assert(read); } }; #endif expected<void> calculate(int i) { if (!i) return std::invalid_argument("i must be non-null"); return {}; } int main() { calculate(0).suppress(); // suppressing must be explicit if (!calculate(1).valid()) return 1; calculate(5); // assert fails }
Even though it might appear new for someone focused solely on C-ish languages, to those of us who had a taste of languages supporting sum-types, it's not.
For example, in Haskell you have:
data Maybe a = Nothing | Just a data Either a b = Left a | Right b
Where the |
reads or and the first element (Nothing
, Just
, Left
, Right
) is just a "tag". Essentially sum-types are just discriminating unions.
Here, you would have Expected<T>
be something like: Either T Exception
with a specialization for Expected<void>
which is akin to Maybe Exception
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With