Hacker News 中文摘要

RSS订阅

Show HN:利用C++26反射实现优雅的类型擦除 -- Show HN: Beautiful Type Erasure with C++26 Reflection

文章摘要

C++26反射库rjk::duck通过声明接口即可实现类型擦除,无需编写大量样板代码。它支持拥有和非拥有语义、运算符、接口组合等功能,只需单头文件包含即可使用,目前仅支持最新编译器。

文章总结

好的,这是根据您的要求,对原文进行的中文重述,保留了核心细节,并删减了与主题无关的冗余内容。


标题:利用 C++26 反射实现优雅的类型擦除

如果你曾尝试过实现比 std::anystd::function 更复杂的类型擦除,你很可能要么写了超过100行极易出错的代码,要么使用了像 Boost.TypeErasureFolly.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::vector{1, 2, 3}}; c.size(); // 3

c = std::string{"hello"}; c.size(); // 5

c = std::map{{1, 2}, {3, 4}}; c.empty(); // false c.clear(); c.empty(); // true ```

只需声明一次接口,剩下的工作就交给已有的定义。该库本身是一个单头文件,提供拥有和非拥有语义、运算符、接口组合、现有接口适配器、第三方类型的扩展方法等功能。

目前,该库仅支持带有 -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_castthis 指针转换回 vtable_function_wrapper,再 static_castduck,从而获取虚函数表和底层数据指针,完成调用。这样,duck 的大小就不再受 trait 函数数量的影响。

constexpr 鸭子:虽然所有函数都标记为 constexpr,但由于指针互转换技巧使用了 reinterpret_cast,这在编译期是不允许的,因此 duck 目前还不能在编译期完美工作。

完整流程回顾

  1. 标签生成members_to_tags 将 trait 转换为多个 has_fn 标签。
  2. 虚函数表生成vtable_generator 为每个标签生成一个函数指针,构成 vtable 结构体。例如,为 std::vector<int> 生成一个包含 sizeemptyclear 函数指针的实例。
  3. 包装器结构体:为每个标签生成一个 vtable_function_wrapper,内部包含一个 vtable_function 对象。
  4. 继承组合:通过继承将所有 vtable_function_wrapper 组合成一个 vtable_wrapper
  5. duckduck 继承自 vtable_wrapper,并存储 void*vtable* 指针。
  6. 调用运算符vtable_functionoperator() 通过指针互转换技巧找到 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++新特性持负面态度,认为其复杂、丑陋、难以调试,且编译时间长。部分评论质疑其实际价值,认为可用更简单的方法替代。少数评论从哲学角度讨论“美”的主观性。