Is this a correct convenience wrapper for std::expected?

265 Views Asked by At

If I am writing a function that returns an std::expected object and potentially calls other functions that return std::expected objects, I find myself writing code snippets like this very common.

struct Foo {  };
std::expected<Foo, std::string> f();

auto res = f();
if(!res) return std::unexpected { res.error() };
auto val = res.value();
// do something with val

So I write a macro like this that "returns" the value in case of success and the error in case of failure.

#define CHECK(expr)\
({\
auto res = expr;\
if(!res) return std::unexpected { res.error() };\
res.value();\
})

I can then use it in this way:

Foo foo = CHECK(f());

I assume the lifetime of the variables in the inner scope should be as long as the assignment expression. Is this correct? Is there any situation where this could go wrong?

1

There are 1 best solutions below

0
On BEST ANSWER

With this macro, you'd write functions like so:

std::expected<Qux, std::string> g() {
  Foo foo = CHECK(f());
  Bar bar = CHECK(b(foo));
  return q(bar);
}
  • The return type can't be deduced from this pattern
  • Understanding the control flow of this code requires knowing (and remembering) what the macro expands to

I'd argue that avoidance of this pattern is what the monadic method std::expected<T,E>::and_then was intended for:

auto g() {
  return f()
      .and_then([](auto foo) { return b(foo); })
      .and_then([](auto bar) { return q(bar); });
}

And in this particular case, it could be shortened even further:

auto g() {
  return f()
      .and_then(b)
      .and_then(q);
}

though realistically, I'd imagine writing out the lambdas would be the more common case in actual code.

Compiler Explorer: https://godbolt.org/z/vovTYfxf4