如何以直观语法正确实现多个std::expected<void, E>的短路组合?
我正在一个包含大量返回bool表示成功与否的函数(如orig_check1、orig_check2)的代码库中引入std::expected,这些底层函数常被同样返回bool的高层函数(如orig_check)调用。我希望为所有函数添加错误码,认为std::expected<void, std::string_view>是合适的选择。
以下程序的check函数中展示了三种组合底层函数check1和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) { // 过于冗长 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 {}; }(); // 复杂度高 std::expected ret2 = check1(val).and_then(std::bind_front(check2, val, 100)); // 因缺少短路求值而不正确 std::expected ret3 = check1(val) && check2(val, 100); // 验证功能等价性 assert(ret1 == ret2 && ret1 == ret3); return ret1; } int main() { check(0); check(1); check(123); return 0; }
我对这些方案都不满意:方案1过于冗长,方案2复杂度高,方案3因缺少短路求值而不正确。理想状态是能使用方案3的语法且具备短路求值特性,请问这是否可行?若不可行,有其他可考虑的方案吗?
一、实现支持短路求值的operator&&
你当前定义的operator&&会先计算左右两边的表达式再传入函数,自然没有短路效果。要实现短路,需要让右边的表达式延迟求值——可以通过接收可调用对象而非直接接收std::expected实例来实现:
template <typename E, typename F> requires std::invocable<F> && std::same_as<std::invoke_result_t<F>, std::expected<void, E>> std::expected<void, E> operator&&(std::expected<void, E>&& lhs, F rhs) { if (!lhs) { return std::move(lhs); } return rhs(); }
这样调用时,右边的函数调用会被包装成lambda延迟执行,只有左边成功时才会触发:
std::expected<void, std::string_view> check(int val) { return check1(val) && [&](){ return check2(val, 100); }; }
这个写法接近你想要的简洁性,同时保留了短路求值特性。
二、使用宏简化链式调用
如果觉得lambda还是有点繁琐,可以用宏来封装短路检查逻辑:
#define CHECK(expr) \ do { \ auto res = (expr); \ if (!res) return res; \ } while(false) std::expected<void, std::string_view> check(int val) { CHECK(check1(val)); CHECK(check2(val, 100)); return {}; }
宏的写法非常直观,和原始的bool检查逻辑风格最接近,几乎没有额外学习成本,适合大规模改造现有代码。
三、C++23的简化方案
C++23中std::expected的and_then可以直接配合lambda简化写法,虽然不如自定义运算符简洁,但无需额外重载:
std::expected<void, std::string_view> check(int val) { return check1(val).and_then([&](){ return check2(val, 100); }); }
四、辅助函数实现链式短路检查
如果不想用宏或重载运算符,可以写一个通用的链式检查辅助函数,利用折叠表达式实现短路:
template <typename E, typename... Fs> std::expected<void, E> chain_check(Fs&&... funcs) { return (std::invoke(std::forward<Fs>(funcs)) && ...); } // 调用方式 std::expected<void, std::string_view> check(int val) { return chain_check( [&](){ return check1(val); }, [&](){ return check2(val, 100); } ); }
内容的提问来源于stack exchange,提问作者MarkB

