C++26 反射落地实战

C++26 反射落地实战:双路线条件编译实现自动路由注册、JSON 序列化与 OpenAPI 文档生成

本文以 Hical 框架(v2.5)为例,展示如何在 C++26 反射尚未被主流编译器完全支持的现阶段,用"C++26 反射 + C++20 宏回退"的双路线策略,让用户享受相同的 API------从 JSON 序列化、路由注册到 OpenAPI 3.0 文档自动生成。


问题:Web 框架中的重复样板代码

每个 Web 框架都有三大类重复劳动:

1. 路由注册------每个处理函数都要手写一行注册:

cpp 复制代码
router.get("/api/users", listUsers);
router.get("/api/users/{id}", getUser);
router.post("/api/users", createUser);
router.put("/api/users/{id}", updateUser);
router.del("/api/users/{id}", deleteUser);
// ... 50 个路由 = 50 行手写注册

2. JSON 序列化------每个 DTO 都要手写字段映射:

cpp 复制代码
json["name"] = user.name;
json["age"] = user.age;
json["email"] = user.email;
// ... 10 个字段 = 10 行手写映射

3. API 文档------每个接口都要手写 OpenAPI 描述,且与代码脱节:

yaml 复制代码
# 改了代码忘了改文档?常见。
paths:
  /api/users:
    get:
      summary: List users
      responses:
        200:
          description: ...

C++26 反射(P2996)可以在编译期枚举结构体的所有成员,彻底消除这些样板代码。但主流编译器尚未正式支持。怎么办?

双路线策略

Hical 的解决方案:用条件编译提供两条路线,对外 API 完全相同

cpp 复制代码
// Reflection.h --- 反射检测
#if defined(__cpp_reflection) && __cpp_reflection >= 202306L
    #define HICAL_HAS_REFLECTION 1  // C++26 反射可用
#elif defined(HICAL_FORCE_REFLECTION)
    #define HICAL_HAS_REFLECTION 1  // 手动强制启用
#else
    #define HICAL_HAS_REFLECTION 0  // 回退到 C++20 宏方案
#endif

用户写的代码长期不变,编译器升级后自动切换到反射路线:

cpp 复制代码
// 用户代码 --- 无论哪条路线都是这样写
struct UserDTO {
    std::string name;
    int age;
    HICAL_JSON(UserDTO, name, age)  // C++26 下此宏变为空操作
};

auto json = meta::toJson(user);           // 两条路线 API 完全相同
auto user = meta::fromJson<UserDTO>(json);

路线一:C++26 反射(未来)

当编译器支持 P2996 时,toJson 的实现极其优雅:

cpp 复制代码
template <typename T>
boost::json::object toJson(const T& obj)
{
    boost::json::object jsonObj;

    template for (constexpr auto member : std::meta::nonstatic_data_members_of(^^T))
    {
        constexpr auto name = std::meta::identifier_of(member);
        jsonObj[name] = valueToJson(obj.[:member:]);
    }

    return jsonObj;
}
  • ^^T --- 获取类型 T 的反射元数据
  • std::meta::nonstatic_data_members_of --- 枚举所有非静态数据成员
  • [:member:] --- 从反射信息恢复为代码实体(访问成员)
  • template for --- 编译期遍历

零宏、零标注、零运行时开销。 编译器在编译期展开循环,生成等价于手写的代码。

C++26 路线还原生支持属性标注:

cpp 复制代码
struct UserDTO {
    [[hical::json_required]] std::string name;
    [[hical::json_name("user_age")]] int age;
    [[hical::json_ignore]] std::string internalId;
};

以及自动 camelCase → snake_case 转换:

cpp 复制代码
auto json = meta::toJsonSnakeCase(user);  // name → name, userId → user_id

路由注册同样:

cpp 复制代码
template <typename Handler>
void registerRoutes(Router& router, Handler& handler)
{
    template for (constexpr auto fn : std::meta::nonstatic_member_functions_of(^^Handler))
    {
        // 读取 [[hical::route("/path", "GET")]] 属性
        // 自动注册到 router
    }
}

路线二:C++20 宏回退(当前)

当前编译器不支持反射,Hical 用宏 + 模板元编程模拟相同的能力。

JSON 序列化的宏方案

cpp 复制代码
struct UserDTO {
    std::string name;
    int age;
    HICAL_JSON(UserDTO, name, age)
    // 展开为:
    // static auto hicalJsonFields() {
    //     return std::make_tuple(
    //         FieldDescriptor<UserDTO, std::string>{"name", &UserDTO::name},
    //         FieldDescriptor<UserDTO, int>{"age", &UserDTO::age}
    //     );
    // }
};

HICAL_JSON 宏的核心是生成一个 hicalJsonFields() 静态方法,返回字段名和成员指针的 tuple。toJson 遍历这个 tuple:

cpp 复制代码
template <typename T>
boost::json::object toJson(const T& obj)
{
    auto fields = T::hicalJsonFields();
    boost::json::object jsonObj;
    constexpr auto count = std::tuple_size_v<decltype(fields)>;

    // 折叠表达式遍历 tuple
    detail::serializeFields(obj, jsonObj, fields, std::make_index_sequence<count>{});
    return jsonObj;
}

折叠表达式展开后等价于:

cpp 复制代码
jsonObj["name"] = valueToJson(obj.name);
jsonObj["age"] = valueToJson(obj.age);

字段装饰器:不只是简单映射

实际项目中,JSON 字段经常需要:别名(前后端命名不同)、必填校验、忽略敏感字段。Hical 提供四种装饰器语法:

cpp 复制代码
struct ApiResponse {
    std::string requestId;
    int statusCode;
    std::string message;
    std::string traceId;

    HICAL_JSON(ApiResponse,
        REQUIRED_ALIAS(requestId, "request_id"),  // 必填 + 别名
        REQUIRED(statusCode),                      // 必填
        ALIAS(message, "status_message"),           // 别名
        HICAL_IGNORE(traceId))                     // 忽略
};
装饰器 含义 序列化 反序列化
name 普通字段 字段名作为 JSON key 缺失则保留默认值
ALIAS(name, "key") 别名 "key" 作为 JSON key "key" 查找
REQUIRED(name) 必填 正常序列化 缺失则抛 runtime_error
REQUIRED_ALIAS(name, "key") 必填+别名 "key" 作为 key "key" 查找,缺失抛异常
HICAL_IGNORE(name) 忽略 跳过 跳过

装饰器的内部实现基于宏标签派发------IS_PAREN 检测参数是否被括号包裹,再分发到对应的 FieldDescriptor 工厂:

cpp 复制代码
// ALIAS(field, "key") 展开后 → ("key", field) → IS_PAREN 为 true → AliasTag
// REQUIRED(field) 展开后 → (field) → IS_PAREN 为 true → RequiredTag
// name → IS_PAREN 为 false → 普通字段

编译期字段校验

写错字段名?编译期就能发现:

cpp 复制代码
struct User {
    std::string name;
    int age;
    HICAL_JSON(User, name, agee)  // typo: agee
};
// 编译错误:
// static assertion failed: HICAL_JSON: field 'agee' does not exist in type 'User'

实现原理是对每个字段包一层 static_assert + requires

cpp 复制代码
#define HICAL_JSON_MAKE_FIELD_(T, field, ...) \
    ([] () { \
        static_assert( \
            requires { std::declval<T>().field; }, \
            "HICAL_JSON: field '" #field "' does not exist in type '" #T "'"); \
        return ::hical::meta::detail::makeField<T>(__VA_ARGS__); \
    }())

可变参数宏的实现:__VA_OPT__ 递归展开

支持大量字段的 HICAL_JSON(Type, f1, f2, ..., fN) 需要一个通用展开器。Hical 采用 __VA_OPT__ 递归展开(C++20 标准特性),而非传统的参数计数方案:

cpp 复制代码
// 递归终止:处理当前参数,若还有剩余参数则继续递归
#define HICAL_JSON_FOR_EACH_AGAIN_() HICAL_JSON_FOR_EACH_HELPER_
#define HICAL_JSON_FOR_EACH_HELPER_(T, a, ...) \
    HICAL_JSON_MAKE_FIELD_(T, a) \
    __VA_OPT__(, HICAL_JSON_FOR_EACH_AGAIN_() (T, __VA_ARGS__))

// 4 层展开:3^5 = 243 个字段上限
#define HICAL_JSON_EXPAND_(...) HICAL_JSON_EXP4_(HICAL_JSON_EXP4_(__VA_ARGS__))
#define HICAL_JSON_EXP4_(...)   HICAL_JSON_EXP3_(HICAL_JSON_EXP3_(__VA_ARGS__))
#define HICAL_JSON_EXP3_(...)   HICAL_JSON_EXP2_(HICAL_JSON_EXP2_(__VA_ARGS__))
#define HICAL_JSON_EXP2_(...)   HICAL_JSON_EXP1_(HICAL_JSON_EXP1_(__VA_ARGS__))
#define HICAL_JSON_EXP1_(...)   __VA_ARGS__

对比旧方案的优势:

旧方案(NARGS 计数) 新方案(__VA_OPT__ 递归)
字段上限 16 个(需逐个手写 FE_1FE_16 243 个(4 层展开自动递归)
维护成本 每增加一个上限需加一组宏 零维护
编译器要求 C++11(但需 MSVC /Zc:preprocessor C++20 __VA_OPT__(所有主流编译器已支持)
代码量 ~50 行宏定义 ~15 行宏定义

路由注册也使用同样的递归展开,支持最多 243 个路由。

路由注册的宏方案

cpp 复制代码
struct UserHandler {
    HttpResponse listUsers(const HttpRequest&) { ... }
    HICAL_HANDLER(Get, "/api/users", listUsers)
    // 展开为:
    // static constexpr RouteInfo hicalRouteInfo_listUsers{HttpMethod::EGet, "/api/users", "listUsers"};

    HICAL_ROUTES(UserHandler, listUsers, getUser)
    // 展开为:
    // static auto hicalRouteTable() {
    //     return std::make_tuple(
    //         RouteRegistrar{hicalRouteInfo_listUsers, &UserHandler::listUsers},
    //         RouteRegistrar{hicalRouteInfo_getUser, &UserHandler::getUser}
    //     );
    // }
};

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

registerRoutes 遍历 hicalRouteTable() 返回的 tuple,逐个调用 router.route()。同时支持同步和协程两种处理器签名:

cpp 复制代码
// 同步
HttpResponse listUsers(const HttpRequest& req) { ... }
// 协程
Awaitable<HttpResponse> createUser(const HttpRequest& req) { ... }

OpenAPI 3.0 自动生成:从反射到文档

v2.5 新增的 OpenAPI 模块让反射层的能力从"代码内部"延伸到了"对外文档"------结构体的字段信息不仅用于 JSON 序列化,还自动生成 OpenAPI Schema,与路由注册结合后产出完整的 API 文档。

四层架构

bash 复制代码
  ┌─────────────────────┐
  │  OpenApiEndpoint.h   │  GET /openapi.json + GET /docs (Swagger UI)
  ├─────────────────────┤
  │  OpenApiDocument.h   │  组装完整 OpenAPI 3.0 JSON(惰性生成 + 缓存)
  ├─────────────────────┤
  │  OpenApiRegistry.h   │  线程安全的路由元数据注册表
  ├─────────────────────┤
  │  OpenApiSchema.h     │  从 HICAL_JSON 的 FieldDescriptor 自动生成 JSON Schema
  └─────────────────────┘

Schema 自动生成

jsonSchema<T>()HICAL_JSON 宏生成的 FieldDescriptor tuple 自动推导 OpenAPI Schema:

cpp 复制代码
struct CreateUserRequest {
    std::string name;
    int age;
    std::string email;
    HICAL_JSON(CreateUserRequest, REQUIRED(name), age, email)
};
HICAL_SCHEMA_NAME(CreateUserRequest, "CreateUserRequest")  // 注册类型名,用于 $ref 引用

生成的 Schema:

json 复制代码
{
    "type": "object",
    "properties": {
        "name":  { "type": "string" },
        "age":   { "type": "integer", "format": "int32" },
        "email": { "type": "string" }
    },
    "required": ["name"]
}

类型映射规则:

C++ 类型 OpenAPI Schema
std::string {"type": "string"}
bool {"type": "boolean"}
int / int32_t {"type": "integer", "format": "int32"}
int64_t {"type": "integer", "format": "int64"}
uint64_t {"type": "integer", "format": "int64", "minimum": 0}
float {"type": "number", "format": "float"}
double {"type": "number", "format": "double"}
std::vector<T> {"type": "array", "items": {...}}
嵌套结构体(有 HICAL_SCHEMA_NAME {"$ref": "#/components/schemas/..."}
嵌套结构体(无名) 内联展开

REQUIRED() 标记的字段自动加入 "required" 数组,HICAL_IGNORE() 标记的字段不出现在 Schema 中。

路由元数据标注

HICAL_API() 宏为路由添加 OpenAPI 描述:

cpp 复制代码
struct ApiHandler {
    Awaitable<HttpResponse> createUser(const HttpRequest& req) { ... }
    HICAL_HANDLER(Post, "/api/users", createUser)

    HICAL_API(createUser,
        builder::summary(info, "Create a new user");
        builder::tags(info, {"users"});
        builder::request<CreateUserRequest>(info, "User data", true);
        builder::response<UserDTO>(info, 201, "Created user");
        builder::responseDesc(info, 400, "Validation error"))

    // 不需要标注的路由用 HICAL_API_DEFAULT
    Awaitable<HttpResponse> healthCheck(const HttpRequest& req) { ... }
    HICAL_HANDLER(Get, "/health", healthCheck)
    HICAL_API_DEFAULT(healthCheck)

    // 使用增强版路由收集宏(同时收集路由表 + API 元数据表)
    HICAL_ROUTES_WITH_API(ApiHandler, createUser, healthCheck)
};

builder 命名空间提供的标注函数:

函数 用途
builder::summary(info, "...") 接口摘要
builder::description(info, "...") 详细描述
builder::tags(info, {"tag1", "tag2"}) 分组标签
builder::request<T>(info, desc, required) 请求体 Schema(从类型自动生成)
builder::response<T>(info, code, desc) 响应 Schema(从类型自动生成)
builder::responseDesc(info, code, desc) 仅描述的响应(无 Schema)
builder::pathParam(info, "id", "string", desc) 路径参数

一键注册

cpp 复制代码
int main()
{
    HttpServer server(8080);
    auto registry = std::make_shared<OpenApiRegistry>();

    // 1. 注册 DTO Schema
    std::unordered_map<std::string, boost::json::object> schemas;
    meta::openapi::registerSchemas<UserDTO, CreateUserRequest>(schemas);
    for (auto& [name, schema] : schemas)
        registry->addSchema(name, std::move(schema));

    // 2. 注册路由 + 收集 API 元数据(一步完成)
    auto handler = std::make_shared<ApiHandler>();
    meta::registerRoutesWithOpenApi(server.router(), handler, *registry);

    // 3. 暴露文档端点
    auto doc = std::make_shared<OpenApiDocument>(
        registry,
        OpenApiConfig{.title = "My API", .version = "1.0.0"});
    serveOpenApi(server.router(), doc);
    // 注册了 GET /openapi.json + GET /docs

    server.start();
}

访问 /docs 即可看到 Swagger UI,所有接口描述、请求体 Schema、响应 Schema 都从代码中自动生成------代码即文档,永不过时

类型安全:不只是序列化

fromJson 在反序列化时做了完整的类型检查:

cpp 复制代码
template <typename T>
T valueFromJson(const boost::json::value& val)
{
    if constexpr (std::is_same_v<T, std::string>)
    {
        if (!val.is_string())
            throw std::runtime_error("JSON type mismatch: expected string");
        return std::string(val.as_string());
    }
    else if constexpr (std::is_integral_v<T>)
    {
        if (!val.is_int64() && !val.is_uint64())
            throw std::runtime_error("JSON type mismatch: expected integer");
        return static_cast<T>(val.as_int64());
    }
    // ...
}

恶意输入 {"age": "not_a_number"} 会得到清晰的错误信息,而不是 Boost.JSON 的底层异常。

配合 REQUIRED() 装饰器,缺失的必填字段同样有明确报错:

cpp 复制代码
auto user = meta::fromJson<UserDTO>(json);
// 若 json 中缺少 "name" 字段:
// std::runtime_error: Missing required field: name

生命周期安全:shared_ptr 管理 Handler

registerRoutes 的路由回调捕获了 handler 的成员函数指针。如果 handler 是栈上的局部变量,server 切到异步模式后 handler 被销毁,回调就会持有悬挂引用。

Hical 提供两个重载:

cpp 复制代码
// 推荐:shared_ptr 管理,生命周期安全
auto handler = std::make_shared<UserHandler>();
meta::registerRoutes(router, handler);

// 便捷:引用版本(调用者保证生命周期)
UserHandler handler;
meta::registerRoutes(router, handler);  // 内部用空删除器包装为 shared_ptr

测试覆盖

反射层和 OpenAPI 模块合计 74 个测试(39 + 35),覆盖:

测试文件 测试数 覆盖范围
test_reflection.cpp 39 JSON 序列化/反序列化、全部装饰器组合、嵌套结构体、向量、uint64 大整数、路由注册与分发、编译期类型特征检测
test_openapi.cpp 35 所有类型的 Schema 生成、装饰器对 Schema 的影响、$ref 和嵌套收集、Registry CRUD、文档组装(路径合并/参数提取/缓存)、端点注册、端到端集成

迁移路径

lua 复制代码
当前 (C++20)                              →  未来 (C++26)
──────────────────────────────────────────────────────────
HICAL_JSON(Type, f1, ALIAS(f2, "k")) 宏   →  自动枚举 + [[hical::json_name]] 属性
HICAL_HANDLER/ROUTES 宏标注               →  [[hical::route(...)]] 属性
HICAL_API() 元数据标注                     →  [[hical::api(...)]] 属性
toJson/fromJson 遍历 tuple                →  template for 遍历反射信息
jsonSchema<T>() 遍历 tuple                →  template for 生成 schema
用户代码无需修改                            →  删除宏标注即可(可选)

这就是双路线的价值:用户代码面向未来,编译器追上来时零改动。

总结

维度 C++26 反射 C++20 宏回退
用户体验 零标注 每个 struct 一行宏
运行时开销 零(编译期展开)
字段装饰器 [[hical::json_*]] 属性 ALIAS / REQUIRED / HICAL_IGNORE
字段数上限 无限 243(__VA_OPT__ 4 层展开)
编译期校验 原生 static_assert + requires
OpenAPI Schema 自动 自动(复用 FieldDescriptor
API 文档生成 自动 HICAL_API() + HICAL_ROUTES_WITH_API()
编译器要求 P2996 支持 C++20
API 完全相同 完全相同

反射不只是语法糖------它是编译期元编程的终极形态 。双路线策略让你今天就能享受它带来的 API 简洁性,同时为编译器支持到位的那天做好准备。而 OpenAPI 集成则证明了反射的价值不止于序列化------从类型信息出发,可以生成一切元数据


源码参考:MetaJson.h / MetaRoutes.h / OpenApiSchema.h / OpenApiRegistry.h

项目地址:github.com/Hical61/Hic...

感谢你读到这里。如果这个系列对你有启发,欢迎在 GitHub 上给个 Star,或者提个 Issue 聊聊你的想法。

相关推荐
Peter·Pan爱编程1 小时前
从 struct 到 class:封装与访问控制的真正意义
c++
计算机安禾1 小时前
【c++面向对象编程】第22篇:输入输出运算符重载:<< 与 >> 的友元实现
java·前端·c++
IvorySQL1 小时前
从 repack.c 深入理解 PostgreSQL REPACK 的底层实现
数据库·postgresql·开源
北山有鸟2 小时前
解决香橙派没有适配ov13855的3A算法
linux·c++·相机·isp
故事和你912 小时前
洛谷-【图论2-1】树4
开发语言·数据结构·c++·算法·动态规划·图论
故事和你912 小时前
洛谷-【图论2-1】树1
开发语言·数据结构·c++·算法·深度优先·动态规划·图论
不会C语言的男孩2 小时前
C++ SLTL编程
java·开发语言·c++
码农-阿杰3 小时前
Java 线程等待唤醒机制深度解析:synchronized、ReentrantLock、LockSupport 底层实现对比
java·开发语言·c++
千桐科技3 小时前
qData 数据中台社区开源版 v1.4.0 发布:元数据管理核心模块正式上线
开源·数据治理·数据集成·数据开发·数据中台·元数据管理·qdata