上周,C++26 在保加利亚索非亚定稿,它将包含我们一直在推动的所有反射相关内容:
- P2996R13:C++26 的反射
- P3394R4:反射的注解
- P3293R3:拼接基类子对象
- P3491R3:
define_static_{string,object,array}
- P1306R5:扩展语句
- P3096R12:C++26 反射中的函数参数反射
- P3560R2:反射中的错误处理
这些是以被采纳的顺序列出,而非按其影响的顺序(否则拼接基类会排在最后)。这是一项相当了不起的成就,若没有很多人的努力是不可能实现的,但在 C++26 的反射方面,没有哪一个人比 Dan Katz 更应负责。
今天要谈论 Dan 在从索非亚回家的航班上(作者在几个座位之外昏迷着)构思的一个非常酷的示例:在编译时摄入一个 JSON 文件并将其转换为 C++ 对象。即给定一个如下的文件test.json
:
{
"outer": "text",
"inner": { "field": "yes", "number": 2996 }
}
可以编写如下代码:
constexpr const char data[] = {
#embed "test.json"
, 0
};
constexpr auto v = json_to_object<data>;
这段代码的结果是现在有一个对象v
,其类型类似:
struct {
char const* outer;
struct {
char const* field;
int number;
} inner;
};
并且其值相应地从 JSON 文件中填充:
static_assert(v.outer == "text"sv);
static_assert(v.inner.number == 2996);
static_assert(v.inner.field == "yes"sv);
这非常酷。
本文其余部分将逐步介绍如何实现这一功能。
让我们从简单开始[](#lets-start-simple)
不是从完整示例开始,而是从一个非常简略的形式开始——这足以演示所有有趣的内容。将解析一个 JSON 对象版本,其中只有一个键和一个值,且该值是一个int
:
consteval auto parse(std::string_view key, int value) -> std::meta::info;
给定一个值如{"x": 1}
,需要采取哪些步骤?最终结果是要生成一个类似的类型:
struct S {
int x;
};
并生成值S{1}
。根据简化的签名,首先获取正确的数据成员和初始化器:
consteval auto parse(std::string_view key, int value)
-> std::meta::info
{
using std::meta::reflect_constant;
auto member = reflect_constant(data_member_spec(^^int, {.name=key}));
auto init = reflect_constant(value);
//...
}
将两个值都包装在reflect_constant
中,尽管data_member_spec
已经是一个meta::info
,这是因为稍后需要解开一层反射。
接下来,data_member_spec
的唯一真正用途是将其传递给define_aggregate
。这里的parse
不是模板,对于每个member
需要一个不同的类型(因为{"x": 1}
和{"y": 1}
需要导致不同的类型)。巧妙的解决方案是:
template <std::meta::info...Ms>
struct Outer {
struct Inner;
consteval {
define_aggregate(^^Inner, {Ms...});
}
};
template <std::meta::info...Ms>
using Cls = Outer<Ms...>::Inner;
consteval auto parse(std::string_view key, int value)
-> std::meta::info
{
using std::meta::reflect_constant;
auto member = reflect_constant(data_member_spec(^^int, {.name=key}));
auto init = reflect_constant(value);
auto type = substitute(^^Cls, {member});
//...
}
substitute(^^Z, {^^Args...})
生成^^Z<Args...>
。即,给定一个模板的反射和模板参数的反射,将返回特化的反射。这就是之前所说的解开一层反射,需要member
是一个表示data_member_spec
值的反射,以便可以直接用表示data_member_spec
的反射实例化Cls
。
type
现在是一个表示每个非静态数据成员集的唯一类型的反射。重要的是,它表示的类型会自动去重并具有外部链接。这可能被证明是从这样的算法内部创建类型的常见习惯用法。
现在有了类型(type
)和初始化器(init
),只需将它们组合在一起,这又是一个substitute
调用:
template <std::meta::info...Ms>
struct Outer {
struct Inner;
consteval {
define_aggregate(^^Inner, {Ms...});
}
};
template <std::meta::info...Ms>
using Cls = Outer<Ms...>::Inner;
template <class T, auto... Vs>
inline constexpr auto construct_from = T{Vs...};
consteval auto parse(std::string_view key, int value)
-> std::meta::info
{
using std::meta::reflect_constant;
auto member = reflect_constant(data_member_spec(^^int, {.name=key}));
auto init = reflect_constant(value);
auto type = substitute(^^Cls, {member});
return substitute(^^construct_from, {type, init});
}
这样就足以使这些断言通过:
static_assert([: parse("x", 1) :].x == 1);
static_assert([: parse("y", 2) :].y == 2);
虽然看起来不多,但为了实现这一点合成了两个具有不同成员的类类型,这很酷。
从一到多[](#from-one-to-many)
现在已经有一个单个的键/值对可以工作,将其推广到任意多个键/值对并不困难。正在代入的两个模板(Cls
和construct_from
)都是可变参数的,所以无需重复。只需推广实现即可。
到目前为止,有一个member
和一个init
,类型都是info
。两者都需要变为vector<info>
:
consteval auto parse(std::string_view key, int value)
-> std::meta::info
{
using std::meta::reflect_constant;
std::vector<std::meta::info> members;
std::vector<std::meta::info> inits;
members.push_back(reflect_constant(
data_member_spec(^^int, {.name=key})));
inits.push_back(reflect_constant(value));
auto type = substitute(^^Cls, members);
inits.insert(inits.begin(), type);
return substitute(^^construct_from, inits);
}
需要将type
插入到inits
的开头,因为construct_from
需要先使用类型实例化,然后再使用所有初始化器。但这种方法有点笨拙,可以通过先在inits
中添加一个占位符,然后再替换它来清理:
consteval auto parse(std::string_view key, int value)
-> std::meta::info
{
using std::meta::reflect_constant;
std::vector<std::meta::info> members;
std::vector<std::meta::info> inits = {^^void};
members.push_back(reflect_constant(
data_member_spec(^^int, {.name=key})));
inits.push_back(reflect_constant(value));
inits[0] = substitute(^^Cls, members);
return substitute(^^construct_from, inits);
}
仍然只有一个键值对,但已经有了处理多个的结构。
之前提到 Boost.JSON 在constexpr
中不工作,但本文的重点不是说明如何解析 JSON,而是如何将 JSON 对象转换为 C++ 结构体。所以将假装 Boost.JSON 可以工作。
不是使用string_view
和int
,而是接受一个boost::json::object
并遍历所有的键/值对。函数的大致结构仍然相同:
consteval auto parse(boost::json::object object)
-> std::meta::info
{
using std::meta::reflect_constant;
std::vector<std::meta::info> members;
std::vector<std::meta::info> inits = {^^void};
for (auto const& [key, value] : object) {
//...
}
inits[0] = substitute(^^Cls, members);
return substitute(^^construct_from, inits);
}
value
现在是一个boost::json::value
,可以是 JSON 值的任何类型。为简单起见,假设只能是(1)一个数字,(2)一个字符串,或(3)一个对象。按顺序处理这些情况。
对于数字情况,看起来很熟悉,因为在之前的部分已经做过了,唯一的区别是数字的来源:
consteval auto parse(boost::json::object object)
-> std::meta::info
{
using std::meta::reflect_constant;
std::vector<std::meta::info> members;
std::vector<std::meta::info> inits = {^^void};
for (auto const& [key, value] : object) {
if (value.is_number()) {
members.push_back(reflect_constant(
data_member_spec(^^int, {.name=key})));
inits.push_back(reflect_constant(
value.to_number<int>()))
} else {
//...
}
}
inits[0] = substitute(^^Cls, members);
return substitute(^^construct_from, inits);
}
由于成员部分在所有情况下都将是相同的——添加一个数据成员,其名称为key
——所以预先进行重构:
consteval auto parse(boost::json::object object)
-> std::meta::info
{
using std::meta::reflect_constant;
std::vector<std::meta::info> members;
std::vector<std::meta::info> inits = {^^void};
for (auto const& [key, value] : object) {
auto add_member = [&](std::meta::info type){
members.push_back(reflect_constant(
data_member_spec(type, {.name=key})));
};
if (value.is_number()) {
add_member(^^int);
inits.push_back(reflect_constant(
value.to_number<int>()))
} else {
//...
}
}
inits[0] = substitute(^^Cls, members);
return substitute(^^construct_from, inits);
}
对于字符串,实际上提出了一个有趣的问题,因为需要能够将初始化器作为常量模板参数传递。这些不能是字符串字面量。此外,当创建嵌套对象时,也需要这些内部对象可用作常量模板参数。这意味着不能使用string_view
——它还不是结构类型。所以将坚持使用char const*
。幸运的是,有一个函数std::meta::reflect_constant_string
可以用来获取一个字符串值并获得具有这些内容的空终止、静态存储持续时间的char
的 constexpr 数组的反射。
consteval auto parse(boost::json::object object)
-> std::meta::info
{
using std::meta::reflect_constant;
std::vector<std::meta::info> members;
std::vector<std::meta::info> inits = {^^void};
for (auto const& [key, value] : object) {
auto add_member = [&](std::meta::info type){
members.push_back(reflect_constant(
data_member_spec(type, {.name=key})));
};
if (value.is_number()) {
add_member(^^int);
inits.push_back(reflect_constant(
value.to_number<int>()))
} else if (auto s = value.if_string()) {
add_member(^^char const*);
inits.push_back(std::meta::reflect_constant_string(*s));
} else {
//...
}
}
inits[0] = substitute(^^Cls, members);
return substitute(^^construct_from, inits);
}
最后是对象情况。给定一个任意的 JSON 对象,如何获得一个到该对象的 C++ 值的反射?已经写过这个函数,就是parse
!递归就是这么酷。在这种情况下,不能立即得到需要生成的对象的类型——但一旦获得了值的反射,之后就可以得到类型:
consteval auto parse(boost::json::object object)
-> std::meta::info
{
using std::meta::reflect_constant;
std::vector<std::meta::info> members;
std::vector<std::meta::info> inits = {^^void};
for (auto const& [key, value] : object) {
auto add_member = [&](std::meta::info type){
members.push_back(reflect_constant(
data_member_spec(type, {.name=key})));
};
if (value.is_number()) {
add_member(^^int);
inits.push_back(reflect_constant(
value.to_number<int>()))
} else if (auto s = value.if_string()) {
add_member(^^char const*);
inits.push_back(std::meta::reflect_constant_string(*s));
} else {
std::meta::info inner = parse(value.as_object());
add_member(remove_const(type_of(inner)));
inits.push_back(inner);
}
}
inits[0] = substitute(^^Cls, members);
return substitute(^^construct_from, inits);
}
这就是全部内容。如果可以使用 Boost.JSON,情况就是这样。
总结[](#wrapping-it-up)
在 Dan 的实际实现(经过一些编辑)中,parse_json
接受一个string_view
并实际必须解析所有的 JSON:
consteval auto parse_json(std::string_view json) -> std::meta::info {
// stuff
}
它遵循了这里呈现的相同结构。还有最后一件事要做:提供一个稍微更好的接口:
struct JSONString {
std::meta::info Rep;
consteval JSONString(const char *Json) : Rep{parse_json(Json)} {}
};
template <JSONString json>
consteval auto operator""_json() {
return [:json.Rep:];
}
template <JSONString json>
inline constexpr auto json_to_object = [: json.Rep :];
就是这样。可以使用新的#embed
引入任意的 JSON 文件并立即将其转换为 C++ 对象:
constexpr const char data[] = {
#embed "test.json"
, 0
};
constexpr auto v = json_to_object<data>;
或者甚至可以直接使用 UDL 操作字符串字面量:
static_assert(
R"({"field": "yes", "number": 2996})"_json
.number == 2996);
基本上实现了一个F# JSON 类型提供程序作为一个相当短的库。当然,它与确切的接口并不完全相同——但 F# 设计实际上只是在此基础上的一个轻微重构。反射是一种全新的语言。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。