一个变量,前一秒装着std::vector<int>,下一秒换成std::string,接口调用照样是c.size()——这不是鸭子类型脚本语言的日常,是C++。开发者Ryan J.K.写的rjk::duck库做到了这件事,靠的是C++26里刚拿到手的反射(reflection)特性,代码量只有一个头文件。
这事有意思的地方不在“又出了个类型擦除库”,而在于它把反射用出了新高度。过去公开的反射示例大多停留在枚举转字符串、JSON序列化这类相对轻的场景,rjk::duck第一次拿反射去自动生成vtable、处理重载决议——这是C++元编程里最硬的骨头之一。
从一个注解到一张vtable
C++类型擦除的老办法只有两条路:用std::any/std::function应付简单场景,或者手写一百多行容易出错的胶水代码,再不然就抱Boost.TypeErasure、Folly.Poly这类模板重库,样板代码同样一堆。
rjk::duck的做法是给接口结构体加一个[[=rjk::trait]]注解,编译器就能在编译期读出这个接口有哪些成员函数,把它们转成内部的tag格式,再用consteval代码块配合新引入的define_aggregate函数,直接生成一张完整的vtable结构体。整个过程不需要开发者手写任何转发代码。
真正棘手的是重载决议。作者没有手动重现C++那套复杂的重载规则,而是巧妙地生成一个overload_set可调用对象,直接把决议工作丢给编译器自己完成——这个思路比手写匹配逻辑聪明得多,也是这篇技术文章里最值得记的一笔。
除了GCC,谁都用不了
这里有个必须说清的落差:这套东西目前只能在GCC上跑,而且要开-std=c++26 -freflection这个实验性编译选项——不是稳定分支,是尚在打磨的前沿功能。另一个同样用C++26反射写的库glaze,同样要求GCC较新版本、同样要开这个实验标志,两个项目凑在一起,恰好印证了当前反射生态还停在单一编译器实验阶段这个事实,不是巧合。
标题里那个“Beautiful”值得琢磨。技术文章本身也承认,反射“目前能力有限”——不能处理重载函数的直接命名冲突,得靠slot_0、slot_1这种编号绕过去;void和const void的区分也要额外补丁。这不是苛责作者,恰恰是这篇技术拆解的诚实之处:它没把反射写成万能钥匙。
- 提醒.
rjk::duck目前查不到独立的社区采用数据、性能对比或第三方代码审查记录,更接近一次严谨的概念验证,还没到可以放心塞进生产项目的阶段。
技术可行性和生产可用性,从来是两回事
古人讲“工欲善其事,必先利其器”,但器再利,也得等到普天下都能拿到手才算数。C++26标准本身是否已经完全定稿、Clang和MSVC什么时候跟进反射提案,这些问题现在都没有清晰答案。GCC一家跑通,说明这条路能走;但一条只有一辆车能开的高速公路,还不算通车。
反射能自动生成vtable,说明C++元编程翻过了一道坎;但翻过坎的只有GCC一家
这个落差不是坏消息,是正常节奏。C++历史上每个大特性——模板、概念(concepts)、协程——都经历过“单编译器先跑、标准委员会追认、其他厂商补课”的三段式,反射大概率也逃不开这个剧本。真正决定rjk::duck这类库能不能进入日常工具箱的,不是这篇技术文章写得多漂亮,而是标准定稿时间表、GCC主线何时默认开放这个特性、以及Clang和MSVC愿不愿意跟。这几件事没一件是作者能单方面推动的。
对还在用Boost.TypeErasure或者手写vtable的团队来说,现在唯一该做的动作是观望,不是迁移——迁移的前提是编译器生态先补齐,而这一步,目前连时间表都还看不清。
