[Hical] 一套 API,两套实现:C++26 反射双轨是怎么设计的

本专栏文章:拆开 Hical · 第 7 篇(完结篇)

这是系列的最后一篇,也是最"前沿"的一篇——C++26 反射在中文技术社区几乎没有实战文章,因为它太新了。Hical 是目前全网唯二(或许唯一)在 open-source 项目中用 C++26 反射的生产级框架。

但 Hical 等不到所有编译器都支持 P2996——它必须兼容 GCC 14、Clang 20、MSVC 2022。所以它做了一套双轨架构:C++26 原生反射和 C++20 宏 fallback 提供相同的用户 API。


1. 问题:你要 JSON 序列化,但不想每个 struct 手写 to_json/from_json

假设你定义了一个 DTO:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
struct User 
{
    std::string name;
    int age;
    std::string email;
};

// 你想要的:
auto json = hical::meta::toJson(user);    // → {"name":"张三","age":25,"email":"z3@example.com"}
auto user2 = hical::meta::fromJson<User>(json);

// 你不想要的:
boost::json::object to_json(const User& u) 
{
    boost::json::object obj;
    obj["name"] = u.name;
    obj["age"] = u.age;
    obj["email"] = u.email;
    return obj;
}
// ↑ 每个 DTO 手写一遍,字段增删时忘了更新序列化 → 静默 bug

传统方案(nlohmann/json)靠宏来自动生成,但需要每个类型手动注册。Hical 的做法是让框架自动发现结构体的字段。


2. 轨道 1:C++26 P2996 原生反射(需要编译器支持)

如果编译器支持 __cpp_impl_reflection >= 202306L__cpp_lib_reflection >= 202306L(P2996 特性测试宏,2025 年 6 月 Sofia 会议定稿),Hical 直接用原生反射:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
template <typename T>
boost::json::object toJson(const T& obj) 
{
    boost::json::object jsonObj;

    // C++26 原生反射:自动枚举所有非静态公开数据成员
    // (P2996R13 合并 P3547R1 访问控制建模后,需传 access_context)
    template for (constexpr auto member :std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unprivileged())) 
    {
        // 检查 [[hical::json_ignore]] 属性 → 跳过
        if constexpr (!detail::isJsonIgnored<member>()) 
        {
            // 获取字段 key:优先 [[hical::json_name("xxx")]],否则原始字段名
            constexpr auto key = detail::jsonKeyOf<member>();
            jsonObj[key] = valueToJson(obj.[:member:]);
        }
    }

    return jsonObj;
}

用户侧:

1
2
3
4
5
6
7
8
struct User 
{
    std::string name;
    int age;
    [[hical::json_ignore]] std::string password;  // 不序列化
    [[hical::json_name("email_addr")]] std::string email;  // 用别名
};
// 不需要宏!编译器通过 ^^T 和 nonstatic_data_members_of 自动发现字段

💡 这是 C++26 反射的 killer feature——不是"宏的替代品",而是"编译器替你写了那几百行重复代码"。


3. 轨道 2:C++20 宏回退(兼容现有编译器)

HICAL_HAS_REFLECTION == 0 时(GCC 14、Clang 20、MSVC 2022 都不支持 P2996),转到宏方案:

3.1 字段注册:HICAL_JSON 宏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct User 
{
    std::string name;
    int age;
    std::string password;
    std::string email;

    // 注册 JSON 序列化字段
    HICAL_JSON(User, name, age, email)
    // ↑ password 不在列表里 → 不序列化
};

3.2 宏展开:VA_OPT 递归

HICAL_JSON(User, name, age, email) 展开成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static const auto& hicalJsonFields() 
{
    static const auto fields = std::make_tuple
    (
        hical::meta::detail::makeField("name", &User::name),
        hical::meta::detail::makeField("age", &User::age),
        hical::meta::detail::makeField("email", &User::email)
    );
    return fields;
}

💡 返回 const auto& 引用 + 内部 static const 局部变量保证单次构造——多次调用不会重复构建 tuple。

每个字段变成一个 FieldDescriptor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
template <typename Class, typename FieldType>
struct FieldDescriptor 
{
    std::string_view name;
    FieldType Class::* pointer;
    bool required = false;
    bool ignored = false;
    // 校验约束
    std::optional<double> minVal;
    std::optional<double> maxVal;
    std::optional<std::string> pattern;
    bool notEmpty = false;
    std::optional<size_t> lengthMin;
    std::optional<size_t> lengthMax;
};

3.3 字段遍历:index_sequence + 折叠表达式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template <typename T, typename Tuple, size_t... I>
void serializeFields(const T& obj, boost::json::object& jsonObj,const Tuple& fields, std::index_sequence<I...>) 
{
    auto serializeOne = [&](const auto& field) 
    {
        if (!field.ignored) 
        {
            jsonObj[field.name] = valueToJson(obj.*field.pointer);
        }
    };
    (serializeOne(std::get<I>(fields)), ...);  // 折叠表达式
}

编译期展开成:

1
2
3
serializeOne(std::get<0>(fields));  // name
serializeOne(std::get<1>(fields));  // age
serializeOne(std::get<2>(fields));  // email

3.4 装饰器语法:ALIAS / REQUIRED / HICAL_IGNORE

1
2
3
4
5
6
HICAL_JSON(User,
    ALIAS(name, "user_name"),         // → json 中的 key 是 "user_name"
    REQUIRED(age),                     // → fromJson 时缺失该字段则报错
    HICAL_IGNORE(password),           // → 显式忽略
    ALIAS(email, "email_addr")        // → 别名 + 正常序列化
)

宏的实现用了 HICAL_IS_PAREN_ 技巧来区分 ALIAS(field, "key") 和普通 field

1
2
3
4
// HICAL_IS_PAREN_: 检测参数是否被括号包裹
#define HICAL_IS_PAREN_(x) ...
// 如果 HICAL_IS_PAREN_(ALIAS(field, "key")) → 走 Tag dispatch 的 ALIAS 路径
// 如果 HICAL_IS_PAREN_(field) → 走普通字段路径

💡 __VA_OPT__(C++20)解决了可变宏参数的"空参数"问题——当字段列表为空时(HICAL_JSON(Empty, )),宏能正确处理 zero-case。


4. 两轨道共享同一套 API

用户代码完全不需要知道用的是哪个轨道:

1
2
3
// 一样的用法
auto user = req.readJson<User>();               // fromJson
auto resp = HttpResponse::json(toJson(status));  // toJson

框架内部通过 HasJsonFields<T> trait 自动检测:

1
2
3
// 两个轨道都产生相同的编译期标记
// C++26: 编译器自动发现 nonstatic_data_members_of
// C++20: HICAL_JSON 宏生成 hicalJsonFields() 静态函数 → HasJsonFields<T> = true_type

5. MetaRoutes:反射驱动的路由注册

同样的双轨架构也用于路由注册:

5.1 C++26 轨道:[[hical::route]] 属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct UserHandler 
{
    [[hical::route("/api/users", "GET")]]
    HttpResponse listUsers(const HttpRequest& req) { ... }

    [[hical::route("/api/users/{id}", "GET")]]
    HttpResponse getUser(const HttpRequest& req) { ... }
};

// 一行注册
hical::meta::registerRoutes(router, handler);

registerRoutes 通过 ^^T 枚举所有成员函数,找到带有 [[hical::route]] 属性的,自动注册到 router。

5.2 C++20 轨道:HICAL_HANDLER + HICAL_ROUTES 宏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct UserHandler 
{
    HttpResponse listUsers(const HttpRequest& req) { ... }
    HICAL_HANDLER(Get, "/api/users", listUsers)

    HttpResponse getUser(const HttpRequest& req) { ... }
    HICAL_HANDLER(Get, "/api/users/{id}", getUser)

    HICAL_ROUTES(UserHandler, listUsers, getUser)
};

hical::meta::registerRoutes(router, handler);

HICAL_HANDLER 展开成 static constexpr RouteInfoHICAL_ROUTES 汇总所有 Handler 到一个 tuple 里。


6. MetaJsonError:为什么从模板中拆分出来?

序列化错误(类型不匹配、字段缺失、验证失败)的处理函数不依赖模板参数 T——它们只需要字段名和错误描述:

1
2
3
4
5
6
7
8
9
// 非模板 [[noreturn]] 函数(注意参数用 std::string_view 而非 const char*)
namespace detail 
{
    [[noreturn]] void throwTypeMismatch(std::string_view expected);
    [[noreturn]] void throwMissingField(std::string_view fieldName);
    [[noreturn]] void throwParseError(std::string_view detail);
    [[noreturn]] void throwValidationErrorNum(std::string_view fieldName, std::string_view rule, double limit);
    [[noreturn]] void throwValidationErrorStr(std::string_view fieldName, std::string_view rule);
}

如果这些函数是模板的一部分(在 MetaJson.h 的模板上下文中),编译器会为每个 HICAL_JSON 类型生成一份独立的错误抛出代码。对 100 个 DTO 类型,就是 100 份相同的 throwTypeMismatch 机器码。

拆分到非模板的 .cpp 文件中后——100 个类型共享同一份错误处理代码。代码体积更小,icache 利用率更高。


7. OpenAPI Schema 双向复用

有了字段描述信息(不管是 C++26 原生反射还是 C++20 宏生成的 tuple),框架可以同时做:

1
2
3
4
5
// 方向 1:JSON 序列化
toJson(user);   // 遍历 FieldDescriptor tuple

// 方向 2:OpenAPI Schema 生成
jsonSchema<User>();  // 遍历同一个 tuple → 生成 JSON Schema

同一个 FieldDescriptor 元组被两个完全不同的系统使用——不需要额外标注。

1
2
3
4
5
6
7
8
9
// jsonSchema<User>() 输出:
{
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": "integer"},
        "email": {"type": "string"}
    }
}

回顾一下你学会了什么

  1. C++26 P2996 反射:^^T + nonstatic_data_members_of(^^T, access_context::unprivileged()) + [[hical::route]] 属性
  2. C++20 宏回退:HICAL_JSON 宏 + makeField + std::index_sequence 折叠表达式
  3. __VA_OPT__HICAL_IS_PAREN_ 的宏技巧解决装饰器语法
  4. 双轨共享用户 API——HasJsonFields trait 自动检测
  5. MetaRoutes:同样的双轨用于路由注册(nonstatic_member_functions_of(^^Handler, access_context::unprivileged())
  6. MetaJsonError 非模板拆分减少每个类型独立的错误处理代码
  7. OpenAPI Schema 和 JSON 序列化共享同一份 FieldDescriptor 元组

系列完结:回顾一下你走完了什么

从第 1 篇的 HTTP 请求热路径到这里,我们拆完了 Hical 的全部核心模块:

主题核心收获
1HTTP 热路径ReadBufferPool borrow/return + picohttpparser 零拷贝 + 响应前缀模板
2Router透明哈希 O(1) + dispatchSync ~40ns + 三层路由优先级
3Middlewaretagged union + buildOptimizedChain 合并 Sync 中间件
4GenericConnectionVyukov MPSC + MpscNodePool + alignas(64) + 写循环 double-check
5IdleScanner集中式扫描替代 10,000 个 timer + Guard RAII + customTimeoutMs
6PMR三级 PMR + StringPool + generation 热更新
7反射双轨C++26 P2996 + C++20 宏 fallback + FieldDescriptor 双重用途

如果你一路跟下来,现在应该能从第一性原理出发,理解一个现代 C++ HTTP 框架的全部核心设计——不是"怎么用",而是"为什么这样设计"。


本系列完结。有问题直接在评论区留言。

开源地址:github.com/Hical61/Hical