基于开源项目复刻的现代C实践——OnceCallback 前置知识上C 基础特性速查这个部分是粉丝请求的本来认为没必要单独发太多了有一些但是想想既然有需求那就做。原本是7篇前置笔者压缩成了两篇。如果想要原本的阅读体验下面的静态网页可以帮到您的忙https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/vol9-open-source-project-learn/chrome/01_once_callback/仓库已经开源仍然在持续建设中喜欢的话点个⭐相关的链接如下https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP静态网页直接阅览https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/引言这一篇是速查手册——我们把 OnceCallback 系列反复用到的 C 基础特性拉出来快速过一遍每个特性只讲三件事“它是什么”、“怎么用”、“OnceCallback 里哪里会用到”。学习目标快速回顾 OnceCallback 系列所需的全部 C11/14/17 基础特性理解函数类型与模板偏特化如何拆解函数签名掌握 std::invoke 的统一调用协议移动语义与 std::move移动语义是整个 OnceCallback 的根基——它是一个 move-only 类型核心设计全靠移动语义撑着。右值引用与移动构造C11 引入了右值引用T它能绑定到临时对象右值上。移动构造函数T(T other)的语义是从other那里把资源偷过来而不是复制一份。偷完之后other进入一个有效但未指定的状态——通常是被清空。classBuffer{int*data_;std::size_t size_;public:Buffer(std::size_t n):data_(newint[n]),size_(n){}// 移动构造偷走 other 的资源Buffer(Bufferother)noexcept:data_(other.data_),size_(other.size_){other.data_nullptr;other.size_0;}~Buffer(){delete[]data_;}};Buffera(100);Buffer bstd::move(a);// b 偷走了 a 的资源a 变空std::move 的本质std::move其实什么都不移动——它只是一个static_castT把传入的对象无条件转换成右值引用。真正执行移动的是移动构造函数或移动赋值运算符。在 OnceCallback 中的应用OnceCallback 的调用方式是std::move(cb).run(args...)——std::move把cb转成右值run()通过 deducing thisC23 特性见下篇检测到这是一个右值调用执行回调并把cb的状态标记为已消费。OnceCallback 删除了拷贝构造和拷贝赋值 delete只保留移动操作。这意味着一个 OnceCallback 对象在任意时刻只有一个持有者——你没法复制它只能通过std::move转移所有权。完美转发与 std::forward完美转发解决的问题是你写了一个函数模板它接受参数并原封不动地传给另一个函数。转发引用与推导规则当函数模板的参数是T且T是模板参数时T不是普通的右值引用而是转发引用也叫万能引用。编译器会根据传入参数的值类别来推导T传入左值x→T intT折叠为int传入右值42→T intT就是intstd::forward 的作用std::forwardT(arg)根据模板参数T的类型决定是返回左值引用还是右值引用templatetypenameTvoidwrapper(Targ){target(std::forwardT(arg));}intx10;wrapper(x);// arg 是左值引用forward 返回左值引用wrapper(10);// arg 是右值引用forward 返回右值引用在 OnceCallback 中的应用bind_once函数模板用它来保持绑定参数的值类别——std::forwardBoundArgs(args)...确保传入的右值仍然是右值传入的左值仍然是左值。可变参数模板与参数包展开可变参数模板让你写出一个接受任意数量、任意类型参数的函数或类。基本语法templatetypename...Types// Types 是参数包voidprint_all(Types...args){// args... 在这里展开// sizeof...(Types) 返回参数数量}Types...叫做参数包parameter pack它可以包含零个或多个类型。args...是函数参数包在调用时展开。在 OnceCallback 中的应用OnceCallbackR(Args...)的Args...就是一个参数包它在类的整个实现中反复出现——构造函数的参数类型、run()的参数类型、内部func_的签名全部来自这个包。函数类型与模板偏特化如果你第一次看到OnceCallbackint(int, int)这个写法大概率会觉得有点奇怪——int(int, int)看起来像函数声明但它出现在模板参数的位置上。函数类型是什么int(int, int)是一种叫做函数类型function type的东西它描述的是接受两个 int 参数、返回 int 的函数。注意它不是函数指针int(*)(int, int)也不是函数引用int()(int, int)——函数类型是比函数指针更底层的概念。static_assert(std::is_function_vint(int,int));// 通过static_assert(!std::is_pointer_vint(int,int));// 通过主模板 偏特化OnceCallback 用了一个两步走的设计// 第一步主模板声明templatetypenameFuncSignatureclassOnceCallback;// 只有声明没有定义// 第二步偏特化版本templatetypenameReturnType,typename...FuncArgsclassOnceCallbackReturnType(FuncArgs...){// 所有真正的代码都在这里};偏特化版本的模式是ReturnType(FuncArgs...)——当FuncSignature能被拆解成这种形式时编译器就选择这个版本。编译器的匹配过程当你写OnceCallbackint(int, int)时编译器看到FuncSignature int(int, int)检查偏特化条件int(int, int)能拆成ReturnType int、FuncArgs {int, int}匹配成功选择偏特化版本这就像类型层面的模式匹配——std::function、std::move_only_function都用了同样的技巧。为什么不用 OnceCallbackR, Args…签名式写法OnceCallbackint(int, int)比参数罗列式OnceCallbackint, int, int更自然——int(int, int)就是一个完整的函数签名读起来一目了然。std::invoke 与统一调用协议OnceCallback 需要接受各种各样的可调用对象普通函数指针、lambda、仿函数、成员函数指针。这些可调用对象的调用语法各不相同——std::invoke提供了一种统一的调用方式。问题调用语法分裂// 普通函数int(*fp)(int,int)add;fp(3,4);// 成员函数指针——语法不同int(Calculator::*pmf)(int,int)Calculator::multiply;(calc.*pmf)(3,4);// 必须用 .* 运算符std::invoke 的分派规则std::invoke(f, args...)根据f和args的具体类型选择正确的调用语法// 成员函数指针 对象引用std::invoke(Calculator::multiply,calc,3,4);// (calc.*multiply)(3, 4)// 成员函数指针 对象指针std::invoke(Calculator::multiply,calc,3,4);// ((*ptr).*multiply)(3, 4)// 普通可调用对象std::invoke([](inta,intb){returnab;},3,4);std::invoke_result_t编译期推导返回类型std::invoke_result_tF, Args...在编译期计算出std::invoke(f, args...)的返回类型autolam[](doublex){returnstd::to_string(x);};usingRstd::invoke_result_tdecltype(lam),double;static_assert(std::is_same_vR,std::string);在 OnceCallback 中的应用bind_once内部用std::invoke来统一处理各种可调用对象——特别是成员函数指针returnstd::invoke(std::move(f),std::move(bound)...,std::forwarddecltype(call_args)(call_args)...);如果不用std::invoke当f是成员函数指针时直接f(bound..., call_args...)会编译失败。enum class 与状态管理enum class是 C11 引入的作用域枚举解决名字污染和隐式转换问题。OnceCallback 用enum class Status来区分回调的三种状态enumclassStatus:uint8_t{kEmpty,// 从未被赋值kValid,// 持有有效的可调用对象kConsumed// 已被 run() 消费};底层类型指定为uint8_t是为了节省内存——整个枚举只占 1 个字节。if constexpr 与编译期分支if constexpr是 C17 引入的编译期条件分支。未选中的分支不会被编译——甚至连语法检查都不会做。templatetypenameRRdo_something(){ifconstexpr(std::is_void_vR){perform_action();return;// void return}else{returnperform_action();}}在 OnceCallback 中if constexpr (std::is_void_vReturnType)用于处理 void 和非 void 返回类型的不同逻辑。decltype(auto)decltype(auto)是 C14 引入的返回类型推导方式。它和auto的区别在于对引用的处理auto会丢掉引用decltype(auto)会保留。intx10;intrefx;autof1(){returnref;}// 返回 int丢掉了引用decltype(auto)f2(){returnref;}// 返回 int保留了引用在 OnceCallback 中bind_once和then()的 lambda 用- decltype(auto)来完美转发可调用对象的返回值。[[nodiscard]] 属性[[nodiscard]]告诉编译器这个函数的返回值不应该被忽略。[[nodiscard]]boolis_cancelled()constnoexcept;OnceCallback 的查询方法都标注了[[nodiscard]]防止调用方手滑写了cb.is_cancelled();而没有使用返回值。Ref-qualified 成员函数C11 允许对非静态成员函数进行引用限定ref-qualifier用或标注在函数参数列表后面。classWidget{public:voidprocess(){// 只能通过左值调用}voidprocess(){// 只能通过右值调用}};在 OnceCallback 中then()方法声明为auto then(Next next) ——末尾的意味着then()只能通过右值调用。小结这一篇我们把 OnceCallback 系列会用到的 C11/14/17 基础特性快速过了一遍。移动语义是 OnceCallback 的根基完美转发保持参数值类别可变参数模板让签名泛化函数类型与模板偏特化拆解签名std::invoke 统一调用协议。下一篇我们进入 C20/23 的高级特性——Lambda 高级特性、Concepts 约束、std::move_only_function 和 Deducing this。参考资源cppreference: 移动语义与右值引用cppreference: std::forwardcppreference: 可变参数模板cppreference: 函数类型cppreference: 模板偏特化cppreference: std::invokecppreference: if constexpr相关阅读移动构造与移动赋值 - 相似度 67%第19篇从输出到输入 —— 为什么按钮比 LED 难 - 相似度 52%