文章摘要
C++26反射库rjk::duck通过声明接口即可实现类型擦除,无需编写大量样板代码。它支持拥有和非拥有语义、运算符、接口组合等功能,只需单头文件包含即可使用,目前仅支持最新编译器。
文章总结
好的,这是根据您的要求,对原文进行的中文重述,保留了核心细节,并删减了与主题无关的冗余内容。
标题:利用 C++26 反射实现优雅的类型擦除
如果你曾尝试过实现比 std::any 或 std::function 更复杂的类型擦除,你很可能要么写了超过100行极易出错的代码,要么使用了像 Boost.TypeErasure 或 Folly.Poly 这样模板代码繁重的库。rjk::duck 利用 C++26 的反射特性,在保留所有自定义和性能的同时,消除了这些痛点。
考虑以下基本示例:
```cpp
include
struct [[=rjk::trait]] Container { auto size() const -> std::size_t; auto empty() const -> bool; auto clear() -> void; };
rjk::duck
c = std::string{"hello"}; c.size(); // 5
c = std::map
只需声明一次接口,剩下的工作就交给已有的定义。该库本身是一个单头文件,提供拥有和非拥有语义、运算符、接口组合、现有接口适配器、第三方类型的扩展方法等功能。
目前,该库仅支持带有 -std=c++26 -freflection 标志的 gcc 编译器。duck 以一些独特的方式使用反射,超越了简单的枚举转字符串或 JSON 序列化示例。本文将揭秘使这个库成为可能的技巧,包括标签生成、虚函数表代码生成、重载决议以及保持 duck 体积小巧的指针互转换技巧。
C++26 反射简介
你可能注意到了示例中的奇怪语法 [[=rjk::trait]]。这是一个 C++26 的注解,可以像属性一样应用于结构体。trait 的定义很简单:
cpp
constexpr inline struct{} trait{};
我们可以通过检查类型是否具有 trait 注解来验证。^^ 运算符产生对某物的反射。duck 的第一步是解释 trait 的成员并将其转换为内部使用的标签格式。例如,对于 trait MyTrait,目标是生成 has_fn<"foo", auto() -> void> 和 has_fn<"bar", auto() const -> int> 这样的标签。这可以通过检查 MyTrait 的成员并进行转换来实现。
生成虚函数表
C++26 的代码生成机制虽然有限,但很强大。vtable_generator 模板会遍历所有 trait 及其标签,并为每个成员函数生成函数指针,从而构建一个虚函数表结构体。为特定类型创建静态虚函数表也相对直接,通过匹配函数签名来填充函数指针。
从槽位到调用:核心的类型擦除技巧与任何其他库相同,即通过一个 erased_call 函数,将 void* 指针转换回具体类型并调用其方法。duck 没有手动实现重载决议,而是生成一个可调用对象 overload_set,让编译器自己处理。candidate_wrapper 类将成员函数调用包装成可调用对象,然后通过 overload_set 组合起来,从而让重载决议在 erased_call 内部由编译器完成。
构建接口
由于反射不允许直接注入成员函数,duck 通过继承一个包装器类型来获得调用语法。这个包装器包含多个 vtable_function 对象,每个对应 trait 的一个成员函数。然而,如果每个 vtable_function 都存储一个指向 duck 的指针,会导致 duck 的大小随 trait 数量线性增长。
指针互转换:解决方案是利用 C++ 标准中的“指针互转换”特性。如果两个类型是标准布局的,并且一个类型的第一个数据成员是另一个类型,那么它们的指针可以互相转换。duck 将每个 vtable_function 放入一个独立的 vtable_function_wrapper 结构体中,并标记为 [[no_unique_address]]。然后,duck 通过继承所有 vtable_function_wrapper 来组装最终的接口。在 vtable_function 的调用运算符中,通过 reinterpret_cast 将 this 指针转换回 vtable_function_wrapper,再 static_cast 到 duck,从而获取虚函数表和底层数据指针,完成调用。这样,duck 的大小就不再受 trait 函数数量的影响。
constexpr 鸭子:虽然所有函数都标记为 constexpr,但由于指针互转换技巧使用了 reinterpret_cast,这在编译期是不允许的,因此 duck 目前还不能在编译期完美工作。
完整流程回顾
- 标签生成:
members_to_tags将 trait 转换为多个has_fn标签。 - 虚函数表生成:
vtable_generator为每个标签生成一个函数指针,构成vtable结构体。例如,为std::vector<int>生成一个包含size、empty、clear函数指针的实例。 - 包装器结构体:为每个标签生成一个
vtable_function_wrapper,内部包含一个vtable_function对象。 - 继承组合:通过继承将所有
vtable_function_wrapper组合成一个vtable_wrapper。 duck类:duck继承自vtable_wrapper,并存储void*和vtable*指针。- 调用运算符:
vtable_function的operator()通过指针互转换技巧找到duck对象,然后通过虚函数表进行实际调用。
所有这些操作相比传统的虚函数分发没有额外的运行时开销。
性能优化
duck 还提供了性能优化选项。通过定义一个 perf_options trait,可以将某些频繁调用的函数指针直接内联存储在 duck 对象中,从而避免一次潜在的冷虚函数表加载。这通过一个 vtable_caller 包装器实现,它根据配置决定是直接调用内联函数指针还是通过虚函数表调用。这虽然会增加 duck 对象的大小,但可以提升性能。
结论
rjk::duck 证明了反射不仅可以减少代码量,还能用更短、更安全、更可调优的代码替代手工编写的复杂机制。该库还有许多其他特性,欢迎查阅其仓库和在线演示。
评论总结
根据评论内容,主要观点和论据如下:
1. 对C++新特性的负面评价(认可度较高) - 评论7(semiinfinitely):“this is the most disgusting programming language ever invented!”(这是有史以来最恶心的编程语言!) - 评论9(YesBox):“I use C++ every day and this feels like an entirely different language and philosophy...my jaw drops a little.”(我每天用C++,但这感觉像完全不同的语言和哲学……让我惊掉下巴。)
2. 对技术实现的质疑(认可度中等) - 评论2(feverzsj):“Reflections, especially static ones, are horrible for debugging.”(反射,尤其是静态反射,对调试来说很糟糕。) - 评论6(gmueckl):“An include with a HTTP URL is a scary abomination straight out of hell.”(包含HTTP URL的include是来自地狱的可怕怪物。)
3. 对实用性的讨论(认可度中等) - 评论3(Leherenn):“What's compilation time like when using it?”(使用它时编译时间如何?) - 评论4(schaefer)询问赋值操作是否调用析构函数:“Does the assignment on line 13 call the destructor for the vector of ints created on line 10?”
4. 对替代方案的看法(认可度较低) - 评论8(usrnm):“Or you just used void* and went on with your life”(或者你直接用void*,然后继续生活) - 评论5(briandilley):“are we still hand writing code?”(我们还在手写代码吗?)
5. 对“美”的哲学思考(认可度低) - 评论1(rob74):“beauty is in the eye of the beholder”(美在观者眼中)
总结: 评论整体对C++新特性持负面态度,认为其复杂、丑陋、难以调试,且编译时间长。部分评论质疑其实际价值,认为可用更简单的方法替代。少数评论从哲学角度讨论“美”的主观性。