如何写出可读性强的C++23单子回调组合代码?
提升C++23单子回调组合代码可读性的方法
你并没有用错单子模式,问题出在匿名Lambda的过度嵌套——这种写法会导致逻辑层级不断加深,形成"回调地狱"。C++23提供了多种特性和技巧,可以扁平化这种单子流程,让代码既保留单子的短路特性,又具备常规写法的可读性。
1. 提取嵌套Lambda为命名辅助函数
最直接的改进是把嵌套的匿名Lambda抽成有明确语义的命名函数/函数对象,让逻辑层级扁平化,同时每个函数的职责更清晰。
改进简单示例(foo/bar相乘)
std::optional<int> multiply_results(int foo_res, int bar_res) { return foo_res * bar_res; } std::optional<int> baz() { return foo().and_then([](int foo_res) { return bar().and_then(std::bind_front(multiply_results, foo_res)); }); }
或者进一步拆分,完全消除内部Lambda:
std::optional<int> get_bar_and_multiply(int foo_res) { return bar().and_then([foo_res](int bar_res) { return foo_res * bar_res; }); } std::optional<int> baz() { return foo().and_then(get_bar_and_multiply); }
改进你的解析器示例
把内部嵌套的Lambda抽成命名函数,拆分后职责更明确:
template <Generator<TokenOrError> Gen> std::optional<grammar::VariableExpression> Parser<Gen>::extract_variable(grammar::Expression expr) { return try_get<grammar::PrimaryExpression>(std::move(expr)) .and_then([](grammar::PrimaryExpression primary_expr) { return try_get<grammar::VariableExpression>(std::move(primary_expr.data)); }); } template <Generator<TokenOrError> Gen> typename Parser<Gen>::maybe_expression Parser<Gen>::parse_assignment_rhs( const token::Equal& equal, const grammar::VariableExpression& variable ) { return expression() .map([variable](std::unique_ptr<grammar::Expression> assign_to) { return std::make_unique<grammar::Expression>(grammar::AssignmentExpression{ variable, std::move(assign_to), variable.line_number }); }); } template <Generator<TokenOrError> Gen> typename Parser<Gen>::maybe_expression Parser<Gen>::assignment() { return equality() .and_then([&](std::unique_ptr<grammar::Expression> expr) { return consume<token::Equal>() .transform([&](const token::Equal& equal) { return extract_variable(std::move(*expr)) .transform([&](const grammar::VariableExpression& variable) { return parse_assignment_rhs(equal, variable); }) .value_or(tl::unexpected{Error{equal.line_number, ErrorType::kBadAssign, fmt::format("Incomplete assignment expression") }}); }) .or_else([&] { return std::move(expr); }) .value(); }); }
2. 利用C++23协程实现线性化流程
C20开始支持协程,C23完善了相关标准库支持。通过自定义支持std::optional/tl::expected的协程任务类型,可以把单子流程写成线性的同步风格代码,自动处理短路逻辑,完全消除嵌套。
自定义支持optional的协程任务
#include <coroutine> #include <optional> template <typename T> struct OptionalTask { struct promise_type { std::optional<T> value; OptionalTask get_return_object() { return {std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_never initial_suspend() { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void return_value(T val) { value = std::move(val); } void unhandled_exception() { std::terminate(); } // 支持co_await std::optional<T>,自动处理nullopt短路 auto await_transform(std::optional<T> opt) { struct Awaiter { std::optional<T> opt; bool await_ready() { return opt.has_value(); } void await_suspend(std::coroutine_handle<>) {} T await_resume() { return std::move(opt).value(); } }; return Awaiter{std::move(opt)}; } }; std::coroutine_handle<promise_type> handle; ~OptionalTask() { if (handle) handle.destroy(); } std::optional<T> get() { handle.resume(); return std::move(handle.promise().value); } };
用协程重写baz()
OptionalTask<int> baz() { int foo_res = co_await foo(); // 自动处理nullopt短路 int bar_res = co_await bar(); co_return foo_res * bar_res; } // 调用方式 std::optional<int> result = baz().get();
用协程重写你的解析器
协程可以把原本嵌套的回调完全改成线性逻辑(需为maybe_expression实现对应await_transform):
template <Generator<TokenOrError> Gen> typename Parser<Gen>::maybe_expression Parser<Gen>::assignment() { auto expr = co_await equality(); if (auto equal = consume<token::Equal>()) { auto variable = co_await extract_variable(std::move(*expr)); auto assign_to = co_await expression(); co_return std::make_unique<grammar::Expression>(grammar::AssignmentExpression{ variable, std::move(assign_to), variable.line_number }); } else { co_return std::move(expr); } }
3. 自定义管道式Monadic组合子
通过重载operator|,可以把and_then/transform等操作改成管道风格,让代码按执行顺序从左到右流动,避免嵌套。
重载operator|用于std::optional
template <typename T, typename F> auto operator|(std::optional<T> opt, F&& func) { return opt.and_then(std::forward<F>(func)); }
管道风格重写baz()
std::optional<int> baz() { return foo() | [](int foo_res) { return bar() | [foo_res](int bar_res) { return foo_res * bar_res; }; }; }
配合std::bind_front还能进一步简化:
std::optional<int> multiply(int a, int b) { return a * b; } std::optional<int> baz() { return foo() | [](int f) { return bar() | std::bind_front(multiply, f); }; }
4. 使用C++23的std::bind_back简化参数绑定
C++23新增的std::bind_back可以更方便地绑定后序参数,配合命名函数减少Lambda的使用:
std::optional<int> multiply(int a, int b) { return a * b; } std::optional<int> baz() { return foo().and_then(std::bind_back(multiply, bar().value())); }
内容的提问来源于stack exchange,提问作者Thornsider3
相关产品推荐
相关产品推荐

