How would you use Alexandrescu's Expected<T> with void functions?

12,087

Solution 1

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:

  • The exception is bound to the value.
  • It's ok to handle the exception as you wish.
  • If the value ignored for some reasons, the exception is suppressed.

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
}

Solution 2

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.

Solution 3

Like Matthieu M. said, this is something relatively new to C++, but nothing new for many functional languages.

I would like to add my 2 cents here: part of the difficulties and differences are can be found, in my opinion, in the "procedural vs. functional" approach. And I would like to use Scala (because I am familiar both with Scala and C++, and I feel it has a facility (Option) which is closer to Expected<T>) to illustrate this distinction.

In Scala you have Option[T], which is either Some(t) or None. In particular, it is also possible to have Option[Unit], which is morally equivalent to Expected<void>.

In Scala, the usage pattern is very similar and built around 2 functions: isDefined() and get(). But it also have a "map()" function.

I like to think of "map" as the functional equivalent of "isDefined + get":

if (opt.isDefined)
   opt.get.doSomething

becomes

val res = opt.map(t => t.doSomething)

"propagating" the option to the result

I think that here, in this functional style of using and composing options, lies the answer to your question:

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?

Personally, I would NOT modify the string in place, or at least I will not return nothing. I see Expected<T> as a "functional" concept, that need a functional pattern to work well: toUpper(s) would need to either return a new string, or return itself after modification:

auto s = toUpper(s);
s.get(); ...

or, with a Scala-like map

val finalS = toUpper(s).map(upperS => upperS.someOtherManipulation)

if you don't want to follow a functional route, you can just use isDefined/valid and write your code in a more procedural way:

auto s = toUpper(s);
if (s.valid())
    ....

If you follow this route (maybe because you need to), there is a "void vs. unit" point to make: historically, void was not considered a type, but "no type" (void foo() was considered alike a Pascal procedure). Unit (as used in functional languages) is more seen as a type meaning "a computation". So returning a Option[Unit] does make more sense, being see as "a computation that optionally did something". And in Expected<void>, void assumes a similar meaning: a computation that, when it does work as intended (where there are no exceptional cases), just ends (returning nothing). At least, IMO!

So, using Expected or Option[Unit] could be seen as computations that maybe produced a result, or maybe not. Chaining them will prove it difficult:

auto c1 = doSomething(s); //do something on s, either succeed or fail
if (c1.valid()) {
   auto c2 = doSomethingElse(s); //do something on s, either succeed or fail
   if (c2.valid()) { 
        ...

Not very clean.

Map in Scala makes it a little bit cleaner

doSomething(s) //do something on s, either succeed or fail
   .map(_ => doSomethingElse(s) //do something on s, either succeed or fail
   .map(_ => ...)

Which is better, but still far from ideal. Here, the Maybe monad clearly wins... but that's another story..

Solution 4

I've been pondering the same question since I've watched this video. And so far I didn't find any convincing argument for having Expected, for me it looks ridiculous and against clarity&cleanness. I have come up with the following so far:

  • Expected is good since it has either value or exceptions, we not forced to use try{}catch() for every function which is throwable. So use it for every throwing function which has return value
  • Every function that doesn't throw should be marked with noexcept. Every.
  • Every function that returns nothing and not marked as noexcept should be wrapped by try{}catch{}

If those statements hold then we have self-documented easy to use interfaces with only one drawback: we don't know what exceptions could be thrown without peeking into implementation details.

Expected impose some overheads to the code since if you have some exception in the guts of your class implementation(e.g. deep inside private methods) then you should catch it in your interface method and return Expected. While I think it is quite tolerable for the methods which have a notion for returning something I believe it brings mess and clutter to the methods which by design have no return value. Besides for me it is quite unnatural to return thing from something that is not supposed to return anything.

Share:
12,087
Alex
Author by

Alex

Updated on June 13, 2022

Comments

  • Alex
    Alex almost 2 years

    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:

    • Have any of you tried Expected<T> in practice?
    • How would you apply this idiom to functions returning nothing (that is, void functions)?

    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?

  • Luc Danton
    Luc Danton about 11 years
    I confess that I implemented my own version with variant<T, std::exception_ptr>.
  • Alex
    Alex about 11 years
    Thanks, but I'm more interested in how the user code would look like - the usage patterns, etc... Though I guess it would include Expected<void>.
  • ixSci
    ixSci about 11 years
    Expected<T> is a bit different, if I've got you right Option[T] is something like C# Nullable, or C++ boost::optional(std::optional proposed). But Expected is for exceptions not for optional values. It is an attempt to get the best of exceptions and return codes.
  • Lorenzo Dematté
    Lorenzo Dematté about 11 years
    @ixSci it looks very similar however: the signature is mostly the same. Don't be confused by the "Option" name: in the end, the border between the two is really blurred (get of an invalid value throws). My purpose is to use it to work through my answer for "How would you apply this idiom to functions returning nothing": I would either not use it (better, I would not use functions that have (only) side effects), or use if-valid/get pairs if you really need to
  • Lorenzo Dematté
    Lorenzo Dematté about 11 years
    Thanks for point out the confusion, I edited a relevant bit (-> Expected<void> assumes a similar meaning: a computation that, when it does work as intended (where there are no exceptional cases), just ends (returning nothing))
  • Bingo
    Bingo about 11 years
    nothrow is supa bad for performance, and should not be used. It is a nice idea in theory, however because it is enforced at run time rather than compile time, it means that a programmer could throw even after declaring nothrow, so the compiler generates a lot of extra code to catch a potential exception and then terminate if one is caught at run time. Catch it, you die. Never throw, worse performance. Until it is enforced at compile time, leave nothrow in the comments.
  • Bingo
    Bingo about 11 years
    And, it is much better to write exception safe code with RAII in mind, and use exceptions exceptionally, than just try catch everything.
  • ixSci
    ixSci about 11 years
    @Bingo, any profs of "super bad for performance"? Actual measures?
  • Alex
    Alex about 11 years
    What's nothrow()? You mean an empty throw()? throw()'s performance problems should be mostly fixed by C++11's noexcept.
  • ixSci
    ixSci about 11 years
    @Alex, thanks I've meant noexcept but forgot its actual name :)
  • Alex
    Alex about 11 years
    Not using functions which have only side effects - well this is a bit difficult with, for example, member setter functions. Basically every member function which modifies the member variables is like that.
  • Lorenzo Dematté
    Lorenzo Dematté about 11 years
    @Alex Obviously, I mean do not use Expected<T> in functions which have only side effects. For example, I would never use it for member setter functions. You do not expect anything from them, are you?
  • Alex
    Alex about 11 years
    @dema80 How would you use them then? Would you throw an exception on error? Or maybe return it (looks a bit like Expected<void>)?
  • Lorenzo Dematté
    Lorenzo Dematté about 11 years
    @Alex it depends. In your specific example (toUpper) I would return a valid string on success, throw on error (also because toUpper is a "non-risky" function: if it fails, something very wrong happened!). For setters, I would definitely throw on error: if a setter fails, there are good chances to have some class-wide invariant broken.
  • Lorenzo Dematté
    Lorenzo Dematté about 11 years
    @Alex in general, I like code where it is possible to see one error handling mechanism, used consistently. But for Expected<T> I would suggest to use it only for functions with a real return value, and avoid Expected<void>. Just throw, in that case (after all, at the end of the computation you are always "getting" the side effect, and get on an invalid Expected<void> will throw)
  • Vicente Botet Escriba
    Vicente Botet Escriba about 10 years
    Just to note that the logic of Expected<void> is inversed respect to Maybe Exception. When Maybe Exception is valid Expected<void> is invalid.
  • einpoklum
    einpoklum about 9 years
    @Bingo: Please be specific about which compilers behave this way. And also, please answer ixSci's comment...
  • v.oddou
    v.oddou about 9 years
    Some guy discussed about the equivalence of Haskell with C++ meta programs somewhere. Notably in the sense that you can write in haskell first to debug, and translate to template later. .. one google search later, actually this seems pretty en vogue, check it out : gergo.erdi.hu/projects/metafun
  • Nir Friedman
    Nir Friedman over 8 years
    If memory serves, throwing in a noexcept function is UB. Hard to imagine that compiler writers would drastically slow down code to support "reasonable" behavior for something that's UB, when they do the exact opposite so often.
  • John Neuhaus
    John Neuhaus over 8 years
    If I'm understanding this correctly, won't the assert fail when trying to chain return values? E.g. expected<void> calculate2(int i) {return calculate(i*2);}
  • John Neuhaus
    John Neuhaus over 8 years
    Also, is there a compelling reason against just surrounding the assert in an #ifndef NDEBUG? Granted you'll have the extra atomic bool in production, but it doesn't seem worth sacrificing readability for.
  • Julien Lopez
    Julien Lopez about 8 years
    any data on performances? cause the way I understood it was the opposite, throw() required to unwind the stack, hence more code added to make sure of it, while noexcept does not, hence no need to add more code from the compiler (or at least, less code). Anyone has more info on what's what?^^
  • Fabio Fracassi
    Fabio Fracassi over 7 years
    even though noexcept is specified to call std::terminate if a function body throws, it does not pose a performance overhead, since the compiler can just replace the throwing of the exception with a call to terminate, and does not have to unwind the stack at all.
  • einpoklum
    einpoklum over 7 years
    @LucDanton: Can you link to that?
  • Luc Danton
    Luc Danton over 7 years
    @einpoklum I think I've coded it away since then. I've been using regular try/catch lately, or optional when there can only be one reason for failure (the latter of which I'm always careful about).
  • Yongwei Wu
    Yongwei Wu over 6 years
    @JohnNeuhaus I think you concern is real, though your current code does not cause the problem to surface (under GCC and Clang). The remedy seems simple though: change the move-constructor to the effect of expected(expected&& o) : spam(std::move(o.spam)), read(o.read.load()) { o.read.store(true); }.