I am attempting to introduce std::expected into a code base that has many functions that return a bool indicating whether the function was successful (e.g. orig_check1 and orig_check2). These functions are frequently invoked by higher level functions that also return a bool (e.g. orig_check).
Specifically, I would like to introduce error codes for all functions. I believe std::expected<void, std::string_view> is a natural choice.
The following program contains 3 different approaches in the check function to combine the results of the lower-level functions (check1 and check2).
#include <cassert>
#include <expected>
#include <string_view>
#include <iostream>
#include <functional>
bool orig_check1(int val) { return val % 2 == 1; }
bool orig_check2(int val, int threshold) { return val <= threshold; }
bool orig_check(int val) { return orig_check1(val) && orig_check2(val, 100); }
////////////////////
std::expected<void, std::string_view> check1(int val)
{
if (val % 2 == 0) {
return std::unexpected{"even"};
}
return {};
}
std::expected<void, std::string_view> check2(int val, int threshold)
{
if (val > threshold) {
return std::unexpected{"too big"};
}
return {};
}
template <typename E>
std::expected<void, E> operator&&(std::expected<void, E> lhs, std::expected<void, E> rhs)
{
return !lhs ? lhs : rhs;
}
std::expected<void, std::string_view> check(int val)
{
// too verbose
auto ret1 = [&]() -> std::expected<void, std::string_view> {
if (auto ret = check1(val); !ret) {
return ret;
}
if (auto ret = check2(val, 100); !ret) {
return ret;
}
return {};
}();
// too complex
std::expected ret2 = check1(val).and_then(std::bind_front(check2, val, 100));
// incorrect due to lack of short-circuit evaluation
std::expected ret3 = check1(val) && check2(val, 100);
// sanity checking functional equivalence
assert(ret1 == ret2 && ret1 == ret3);
return ret1;
}
int main()
{
check(0);
check(1);
check(123);
return 0;
}
I'm not thrilled about any of these approaches. #1 is too verbose. #2 is too complex. #3 is incorrect. Ideally, I would be able to write the syntax of #3 with short-circuit evaluation. Is this possible? If not, is there a different approach that should be considered?
First, just to have said it:
If your unexpected result paths are truly unexpected (or dare I even say exceptional), you could consider just using exceptions.
With that out of the way, I actually had a quite similar situation recently and considered two other approaches from the ones you tried.
This is a similar idea to @Caleth's answer, but uses lambdas to avoid the overhead of std::function: To get the short-circuiting behavior, we need to delay the actual invocation of check2 until we are sure that check1 succeeded. This can be done by wrapping the call to check2 in a lambda:
[&]() -> decltype(auto) { return check2(val, 100); }
and since writing that lambda every time can be quite annoying and error prone, you could hide it in a macro (and even add on correct noexcept propagation as a bonus):
#define LAZY(...) \
[&]() noexcept(noexcept(__VA_ARGS__)) -> decltype(__VA_ARGS__) { \
return __VA_ARGS__; \
}
This lambda is then some invocable type that yieds a std::expected. We can use that to define the && operator as follows:
template <typename E, typename Lazy>
requires std::is_invocable_r_v<std::expected<void, E>, Lazy>
std::expected<void, E> operator&&(std::expected<void, E> lhs, Lazy&& rhs)
{
return !lhs ? lhs : std::forward<Lazy>(rhs)();
}
Your check method could then look as follows:
std::expected<void, std::string_view> check(int val) {
return check1(val) && LAZY(check2(val, 100));
}
here is a working example on godbolt.
I tried (ab)using co-routines to make expecteds awaitable and short-circuit in case they are in the unexpected state. Your check method could then look like this:
Expected<void, std::string_view> check(int val)
{
co_await cehck1(val);
co_await check2(val, 100);
co_return {};
}
This would have the desired short-circuiting behavior and, while a little unusual at first, is quite straight forward once you have seen it a few times.
There are a few caveats however:
Expected instead of std::expected. This is because you have to specialize std::coroutine_traits for the return type and specializing types in std is only allowed when using at least one custom type.clang is able to do it somewhat reliably).If all that doesn't deter you, here is the godbolt playground I used to test this. It is by no means a complete implementation (no error checks, constraints, etc...), but could give you a good starting point.
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