news 2026/5/12 8:51:10

基于开源项目的现代C++实战——OnceCallback 实战(五):then 链式组合

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于开源项目的现代C++实战——OnceCallback 实战(五):then 链式组合

基于开源项目的现代C++实战——OnceCallback 实战(五):then 链式组合

仓库已经开源!仍然在持续建设中,喜欢的话点个⭐!相关的链接如下:

clone me!: git clone https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP

静态网页体验极大改进,点击这里直接阅览:https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/

引言

then()允许我们把两个回调串联成一个管道——第一个回调的输出是第二个回调的输入。听起来简单,但它是 OnceCallback 四个功能中所有权设计最精巧的一个。因为 OnceCallback 是 move-only 的,then()必须把原回调的所有权完整地转移到新回调中,不能有任何共享或泄露。

这一篇我们从管道思维出发,逐行拆解then()的实现,重点理解所有权链和 void/非 void 分支的处理。

学习目标

  • 理解then()的管道语义和所有权链设计
  • 逐行理解then()的完整实现
  • 理解 void 前缀回调的特殊处理
  • 对比then()&&限定和run()用 deducing this 的选择理由

管道思维:then() 的语义

如果你用过 Unix 管道,then()的语义就很直觉了:

# Unix 管道:cmd1 的输出是 cmd2 的输入echo"hello"|tr'h''H'|wc-c

then()做的是同样的事情——回调 A 的输出是回调 B 的输入。用代码表达:

autopipeline=OnceCallback<int(int,int)>([](inta,intb){returna+b;// 第一步:3 + 4 = 7}).then([](intsum){returnsum*2;// 第二步:7 * 2 = 14});intresult=std::move(pipeline).run(3,4);// result == 14

then()把两个独立的回调串联成一个新的回调。调用新回调时,自动走完 A → B 的整个流程。


所有权是 then() 的核心挑战

串联后的新回调需要持有原回调和后续回调的所有权——否则原回调可能在外部被提前消费掉,管道就断了。而 OnceCallback 是 move-only 的,这意味着then()必须消费*this(原回调)和next(后续回调),把两者的所有权转移到一个新的 lambda 闭包里。

整个所有权链条是这样的:

新 OnceCallback → move_only_function → lambda 闭包 → [原回调 + 后续回调]

每一层都通过移动语义传递所有权,没有任何共享或拷贝。这就是 move-only 语义在then()中的完整体现。


then() 的完整实现逐行拆解

template<typenameReturnType,typename...FuncArgs>template<typenameNext>autoOnceCallback<ReturnType(FuncArgs...)>::then(Next&&next)&&{usingNextType=std::decay_t<Next>;ifconstexpr(std::is_void_v<ReturnType>){usingNextRet=std::invoke_result_t<NextType>;returnOnceCallback<NextRet(FuncArgs...)>([self=std::move(*this),cont=std::forward<Next>(next)](FuncArgs...args)mutable->NextRet{std::move(self).run(std::forward<FuncArgs>(args)...);returnstd::invoke(std::move(cont));});}else{usingNextRet=std::invoke_result_t<NextType,ReturnType>;returnOnceCallback<NextRet(FuncArgs...)>([self=std::move(*this),cont=std::forward<Next>(next)](FuncArgs...args)mutable->NextRet{automid=std::move(self).run(std::forward<FuncArgs>(args)...);returnstd::invoke(std::move(cont),std::move(mid));});}}

函数签名:右值限定

autothen(Next&&next)&&

末尾的&&使其成为右值限定的成员函数——只能通过std::move(cb).then(next)或临时对象.then(next)调用。如果调用方写了cb.then(next)(左值调用),编译器直接报"没有匹配的重载函数"。这是表达消费语义的另一种方式——和run()用 deducing this 不同,then()不需要区分左值和右值给出不同的错误信息,直接用 ref-qualifier 更简洁。

std::decay_t<Next>:退化去掉引用

usingNextType=std::decay_t<Next>;

Next可能是SomeLambda&&(右值引用)或SomeLambda&(左值引用),std::decay_t把引用去掉,得到裸的 lambda 类型。后续用NextType做类型查询。

if constexpr 的两个分支

then()的核心区别在于原回调的返回类型是不是 void。

非 void 分支:原回调返回一个值,这个值需要传给后续回调。

usingNextRet=std::invoke_result_t<NextType,ReturnType>;

std::invoke_result_t<NextType, ReturnType>在编译期推导"把ReturnType类型的值传给NextType类型的可调用对象,返回什么类型"。这就是新回调的返回类型。

lambda 内部的执行流程:先调用原回调拿到中间结果mid,再把mid传给后续回调。

automid=std::move(self).run(std::forward<FuncArgs>(args)...);returnstd::invoke(std::move(cont),std::move(mid));

void 分支:原回调没有返回值,后续回调不接受参数。

usingNextRet=std::invoke_result_t<NextType>;

std::invoke_result_t<NextType>推导的是"不带参数调用NextType,返回什么类型"。

lambda 内部的执行流程:先执行原回调(不拿返回值),再执行后续回调(不传参数)。

std::move(self).run(std::forward<FuncArgs>(args)...);returnstd::invoke(std::move(cont));

lambda 捕获:所有权的核心

[self=std::move(*this),cont=std::forward<Next>(next)]

self = std::move(*this)是整个所有权链的关键——它把当前 OnceCallback 对象的所有内容func_status_token_)移动到 lambda 的闭包对象里。移动之后,当前对象进入"被移走"的状态——func_token_已经被搬走了。

cont = std::forward<Next>(next)把后续回调也搬进 lambda 闭包。std::forward保持next的值类别——右值就移动,左值就拷贝。

这个 lambda 又被传给一个新的OnceCallback<NextRet(FuncArgs...)>构造函数,存入新回调的std::move_only_function里。move_only_function的类型擦除能力保证了不管 lambda 的实际类型是什么,都能被统一存储。


多级管道

then()可以链式调用,形成多级管道:

usingnamespacetamcpp::chrome;autopipeline=OnceCallback<int(int)>([](intx){returnx*2;}).then([](intx){returnx+10;}).then([](intx){returnstd::to_string(x);});std::string result=std::move(pipeline).run(5);// 5 * 2 = 10, 10 + 10 = 20, to_string(20) = "20"

每次then()都会创建一个新的 OnceCallback,内部嵌套捕获了前一步的回调。调用最外层的run()时,执行过程是递归展开的:最外层回调被run()→ 执行其 lambda → lambda 内部对上一层调用std::move(self).run()→ 再对更上一层调用 → 直到底层。

性能上,每一层then()增加一次std::move_only_function的间接调用。对于 2-3 级的管道来说完全可接受。如果管道层级超过 10 级,可能需要考虑扁平化的管道结构来避免过深的嵌套——但这已经超出我们当前的讨论范围了。


几个容易踩坑的地方

mutable 不可省略

lambda 内部需要调用std::move(self).run()——这个操作会修改self的状态(把 status 从 kValid 改为 kConsumed)。如果 lambda 是 const 的(没加mutable),self在内部就是 const 引用,没法在 const 对象上调用修改状态的操作,编译直接失败。

self = std::move(*this) 的状态

移动之后,当前 OnceCallback 对象的func_token_都已经被 move 走了——它们处于"被移走"的状态。status_没有被显式设为 kEmpty,而是保持原来的值。但因为func_已经被 move 走了,当前对象实际上已经不可用了——任何对它的操作都是未定义的。then()&&限定保证了调用方没法在调用then()之后继续使用原对象。

为什么用 std::invoke 而不是直接调用

cont是一个普通可调用对象(通常是 lambda),直接cont(mid)也能工作。但std::invoke是防御性编程——如果有人传进来一个成员函数指针作为后续回调,直接调用语法会失败,std::invoke不会。统一使用std::invoke保证了无论传什么可调用对象都能正确工作。


小结

这一篇我们拆解了then()的完整实现。它的核心挑战是所有权管理——通过self = std::move(*this)把整个原回调搬进 lambda 闭包,建立完整的所有权链。if constexpr处理 void 和非 void 返回类型的不同语义——void 回调不传参数给后续回调,非 void 回调传递中间结果。then()&&限定表达消费语义(比run()的 deducing this 更简洁,因为不需要自定义错误信息),mutable关键字不可省略(因为内部需要修改self的状态)。

下一篇是系列的最后一篇——我们用系统化的测试用例来验证整个实现,并对比与 Chromium 原版的性能差异。

参考资源

  • Chromium callback.h 源码
  • cppreference: std::invoke
  • cppreference: if constexpr
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/12 8:38:35

AI助手前端开发利器:assistant-ui/tool-ui组件库深度解析与应用实践

1. 项目概述与核心价值最近在折腾AI应用开发&#xff0c;特别是想给大语言模型&#xff08;LLM&#xff09;加个“手”和“眼”&#xff0c;让它不仅能说会道&#xff0c;还能调用外部工具、处理文件、展示图表。市面上现成的UI组件库不少&#xff0c;但专门为AI助手&#xff0…

作者头像 李华
网站建设 2026/5/12 8:32:39

Chinese Abacus (Chinese Zhusuan)

Chinese Abacus &#xff08;Chinese Zhusuan&#xff09; 小学珠算&#xff0c;中华民族的瑰宝&#xff0c;这玩意可以计算无限大的数据&#xff0c;如果把这个串串弄得足够多

作者头像 李华
网站建设 2026/5/12 8:28:29

嵌入式设备:AirUI 节气动画与交互设计实践

本文将完整基于8101引擎主机开发的24节气APP的全开发流程&#xff0c;以合宙Air8101畅玩板作为硬件载体&#xff0c;搭载LuatOS系统提供稳定运行支撑&#xff0c;依托AirUI轻量化图形框架实现流畅交互与简约设计&#xff0c;核心围绕中华传统二十四节气文化展开&#xff0c;将传…

作者头像 李华