最近,Matus Chochlik[1] 在 clang 的一个分叉中实现了《C++ 反射扩展[2] N4856》技术规范(或 TS),可以在这里[3]把玩。我自然对此很感兴趣,但相关的 cppreference[4] 页面看起来还是非常空洞。
因此,在花了一点时间研究这个 TS 之后,我想解释一下它到底是怎么回事,以及如何用它来正经做些事情。
在这篇文章中,我将解释该规范的基本想法,如何编写一个简单的泛型“枚举到字符串”函数,并略微深入探究一个概念验证性质的序列化函数的细节。
请注意,在所有的代码中,我用 namespace reflect = std::experimental::reflect 以求简短。另外,我还是建议在电脑上而不是在移动设备上阅读本文。
基本上整件事情都基于概念,而概念的本质是对通用类型的约束。我这里不做太详细的介绍,但对于不熟悉的人来说,这篇文章[5] 是很好的介绍,你也可以在这里[6]找到更多关于概念的文档。
反射 TS 增加了一个新的关键字,reflexpr,它返回一个所谓的“反射元对象类型”,基本上就是一个符合 Object 概念的类型。还有其他一些概念对 Object 进行精化,比如 Variable、ObjectSequence、Lambda 等。在这里,精化只是意味着“约束得更多”。
一旦你有了元对象类型,就可以通过元函数询问它一些东西,比如 get_name、is_class、get_public_data_members 等等。所有这些元函数都受到相当合理的约束,例如,你只能对 Record 调用 get_public_data_members,而 Record 是约束了类、结构体和联合体的一个概念。请注意,和其他的 C++ 标准元函数一样,这些元函数通常有 _t 或 _v 的简写形式,所以你可以写出比如 stuff_v
枚举只有三个专门的元函数,而这里我们只关注 get_enumerators,它会给我们一个 ObjectSequence 类型以便进一步处理:(下滑滚动看完整代码,下同)
template<typename T> // 令人困惑的是,还有个 reflect::is_enum(_v),它告诉你某个元对象是不是 reflect::Enum
consteval auto enum_names() requires std::is_enum_v<T>
{
// 这就是 reflexpr;reflected_enum 是一个元对象,它反射 T
using reflected_enum = reflexpr(T);
// enum_enumerators 是个 ObjectSequence,它包含每个枚举值的元对象类型
using enum_enumerators = reflect::get_enumerators_t<reflected_enum>;
// 更多内容马上来
}
ObjectSequence 是一个代表一系列 Object 的概念;它本身有几个元函数,特别是 get_element。有了它,我们可以写一个函数,对 ObjectSequence 中每个元素应用另一个元函数并将结果装入一个数组:
template<
// 我们要应用的特征,请别太担心语法
template<typename> typename Trait_t,
// 将要应用它到这个序列
reflect::ObjectSequence Sequence_t,
// 帅帅的变参,这样我们可以对每个元素应用这个运算
size_t... ints>
consteval auto make_object_sequence_array(std::index_sequence<ints...>)
{
// Trait_t 得有一个 value 成员
return std::array { Trait_t<reflect::get_element_t<ints, Sequence_t>>::value... };
}
这有点不好马上消化,但本质上它只是将 Trait_t 应用于 Sequence_t 的每个元素。index_sequence 需要一个大小,这我们可以通过 get_size 元函数获得,该元函数会给出 ObjectSequence 中的元素数量。现在就可以调用 make_object_sequence_array:
template<typename T>
consteval auto enum_names() requires std::is_enum_v<T>
{
using reflected_enum = reflexpr(T);
using enum_enumerators = reflect::get_enumerators_t<reflected_enum>;
// 拿到 ObjectSequence enum_enumerators 的大小
constexpr auto T_size = reflect::get_size_v<enum_enumerators>;
using sequence = std::make_index_sequence<T_size>;
// 对 ObjectSequence 中的每个元素应用 get_name, 结果以数组形式返回
return make_object_sequence_array<reflect::get_name, enum_enumerator>(sequence{});
}
在 Compiler Explorer[7] 上测试,我们看到它完美地工作了。现在就可以颇为轻松地写出我们的 enum_to_string 函数:
template<typename T>
constexpr auto enum_to_string(const T value) requires std::is_enum_v<T>
{
// 这里又有点容易搞混,有个 reflect::underlying_type,它返回的是某 reflect::Enum 的底层类型的元对象
using underlying_type = std::underlying_type_t<T>;
const auto underlying_value = static_cast<underlying_type>(value);
// 轻松
return enum_names<T>()[underlying_value];
}
我就此搁笔,直到 Barry Revzin[8] 提出来,嗯,这压根不行!它假定枚举从零开始,而且所有的值都是连续的,但情况并不总这样,所以得返工。可以通过在我们的元函数中使用 get_constant 来解决这个问题,它会返回一对常量和名称,像这样:
template<typename T> requires reflect::Constant<T> && reflect::Named<T>
struct get_constant_and_name
{
static constexpr auto value = std::pair { reflect::get_constant_v<T>, reflect::get_name_v<T> };
};
把 enum_names 中的 get_name 换成 get_constant_and_name,现在它返回由 pair 组成的数组,这样就行了[9]。现在我们需要对 enum_to_string 函数做点修改,就成了:
template<typename T>
constexpr auto enum_to_string(const T value) requires std::is_enum_v<T>
{
// 此处可以是个 std::find_if, 甚至是 ranges::find,但让我们相对从简
for(const auto&pair : enum_names<T>())
{
// 提醒下, first 是值, second 是名称
if(value == pair.first)
{
return pair.second;
}
}
// 比如用标志位做入参调用可能就会这样
return "Unnamed value";
}
注意,这仍然不能处理一个枚举中有两个枚举项值相同的情况。
这里不会有任何实际的序列化代码——我把它作为练习留给读者。那么,要快速展示的是如何迭代遍历一个类型并相当自动地将其分解为可序列化的部分。为此,我们将使用一个递归的模板函数,草稿我们可以这样打:
template<typename T>
void serialize(const T& value)
{
// Collection 是我写的一个概念,如你对其实现感兴趣,请稍后留意 Compiler Explorer 链接
if constexpr(Collection<T>)
{
// 对每个元素调用 serialize,很容易
for(const auto& element : value)
{
serialize(element);
}
} else
if constexpr(std::is_class_v<T>)
{
// 在这里我们应该将 T 分解为它的成员,分离出成员变量并对他们调用 serialize
} else
{
// 在这里我们处理那些原始类型
}
}
处理原始类型基本上只是在一堆 if constexprs 里处理字符串、算术类型、枚举(对于它,也许应该使用我们的 enum_to_string 函数!),等等。
有意思的部分在于如果 T 是一个类。这时 get_public_data_members 就能派上用场了:
if constexpr(std::is_class_v<T>)
{
// 这又是一个反射 T 的元对象类型
using Reflected_t = reflexpr(T);
// 也许我们并不想序列化 private 成员,尽管也有支持它们的 get_data_members
using data_members = reflect::get_public_data_members_t<Reflected_t>;
// 又是从大小中得到一个 index_sequence
constexpr auto T_size = reflect::get_size_v<data_members>;
using sequence = std::make_index_sequence<T_size>;
// 然后呢?
}
这里有点棘手。我们想对 data_members 的每个成员使用 get_pointer 从而得到指向类数据成员的指针[10],但是如果我们把 get_pointer 用在一个元对象类型之上,它就没有 value 成员,因为元对象类型并不是个 Variable。我通过创建自己的元函数包装 get_pointer 来绕过这个问题。如果它的 T 符合概念 Variable,那么它就调用 get_pointer。否则,它返回 std::monostate,表示这个元对象应该被忽略:
template<typename T>
struct get_pointer_or_monostate
{
private:
static constexpr auto get_value()
{
if constexpr(reflect::Variable<T>)
{
return reflect::get_pointer<T>::value;
}
else
{
return std::monostate{};
}
}
public:
static constexpr auto value = get_value();
};
有了这个新的(也许非常绕的,如果你找到了更好的方法,请告诉我)元函数,我们现在可以用它来获取数据成员指针和 monostate 组成的元组:
// 和 make_object_sequence_array 一样,只是现在返回一个 std::tuple
constexpr auto pointer_or_monostate_tuple = make_object_sequence_tuple<get_pointer_or_monostate, data_members>(sequence{});
// 下面是将模板化的 lambda 表达式应用到元组的每个元素上去( 更好地施展 std::apply 法力的一种写法 )
apply_operation_on_tuple([&value](auto current_value)
{
using current_value_t = decltype(current_value);
// "not" 挺酷的,来告我呗;这里我们检查 current_value 类型,如果不是 monostate,意味着它是指向某个类数据成员的指针
if constexpr(not std::same_as<current_value_t, std::monostate>)
{
// 现在就清楚了,我们能够通过使用指向类数据成员的指针得到该成员的值
const auto& member = value.*current_value;
// 这样我们就可以对该成员调用 serialize
serialize(member);
}
}, pointer_or_monostate_tuple);
好,大致上这就可以了。把这些扔进 Compiler Explorer[11],我实际上还没有写任何序列化代码,但放了几个 std::cout 进去,打印表明我们正确地遍历要序列化的数据,行了!
我对 C++ 最感到懊恼的方面之一是它缺乏作为语言核心部分的(好的——对不起,typeid)反射。编译器一定知道的简单东西,比如枚举名称,通常要用当前的 C++ 手动编写,或者不得不依赖像 magic enum[12] 这样的库,并承受由此带来的所有缺点。
就我所知,这个 TS 的命运还没有决定。显而易见的是,我刚才展示的代码并不容易懂,也不容易编写。和使用模板一贯的体验一样(尽管概念已经改善了它),找出某个东西哪里不工作是非常痛苦的。还有对编译速度的担忧,因为这是大量的使用泛型的编译期工作。
尽管如此,TS 为 C++ 特有的许多流行弊病提供了一个难以置信的强大解决方案,所有这些都在一个一致的、建立在标准其他部分之上的软件包中,并且它以大多数模板爱好者相当容易辨识的方式做到了这点。比如,我没有提到对命名空间的反射,但是你能用它做成的事情是难以想象的。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/YeAopvn0eLeq_z6pjnp9TQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。