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_1 到 FE_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 上给个 Star,或者提个 Issue 聊聊你的想法。