使用yalantinglibs中的反射库格式化自定义struct/enum
2024-07-20
目录
yalantinglibs是一个c++20的库集合。 std::formatter是c++20的新特性,用来格式化内容。
这里尝试使用ylt里的反射库实现自定义struct/enum的格式化输出
std::format的基本用法
基本用法
头文件1#include <format>
一眼看起来和python rust的格式化语法很类似。实际用起来,受限制很多。比如std::format的控制字符串需要编译期检查,没法运行时生成,也没有python的1print(f"a = {a}")
(这叫啥?),参数只能放到后面1std::println("a = {}", a)
。
总之,比没有好 。
1#include <format>
2#include <string>
3
4
5int main(){
6 int a = 42;
7 std::string b{"Hello World!"};
8 float c = 10.24;
9 std::cout << std::format("a = {}, b = {}, c = {:.4f}\n", a, b, c);
10 return 0;
11}
输出内容
1a = 42, b = Hello World!, c = 10.2400
2
扩展用法
自定义的struct/enum都没有默认支持,需要自定义1std::formatter<T>
来实现。
具体来说,需要特化1std::formatter<T>
,并实现他的两个函数
1parse()
实现如何解析类型的格式字符串说明符1format()
为自定义类型的对象/值执行实际格式化
1#include <iostream>
2#include <format>
3#include <string>
4
5struct Person {
6 std::string name;
7 int age;
8};
9
10// 定义Person的formatter
11template<>
12struct std::formatter<Person> {
13 static constexpr auto parse(std::format_parse_context& ctx) {
14 return ctx.begin();
15 }
16
17 auto format(const Person& p, std::format_context& ctx) const {
18 return std::format_to(ctx.out(), "Person(name: {}, age: {})", p.name, p.age);
19 }
20};
21
22int main() {
23 Person p{"Alice", 30};
24 std::string s = std::format("{}", p);
25 std::cout << s << std::endl;
26
27 return 0;
28}
29
输出
1Person(name: Alice, age: 30)
2
其他用法
太多了,不给自己挖坑了。推荐看这个c++20 compelte guide
1std::vformat() 和 vformat_to()
- 格式字符串的语法
- 标准格式说明符:
1fill align sign # 0 width .prec L type
- 全局化,语言环境
- 错误处理
- 自定义格式
- 自定义格式的解析格式字符串
yalantinglibs的reflection
官方示例test_reflection
文章C++20 非常好用的编译期反射库,划重点【没有宏,没有侵入式】
几个api
1constexpr auto sz = ylt::reflection::members_count<S>();
获取S的成员个数。编译期计算。-
1template <typename T, typename Visit> inline constexpr void for_each(Visit func)
遍历1struct T
的每一个成员。Visit func可以是
1[](auto& field) {}
1[](auto &field, auto name, auto index) {}
1[](auto& field, auto name) {}
1constexpr auto type_name = ylt::reflection::type_string<S>();
获取S的类型字符串
有这几个就可以写出struct的formatter了。
int[]的formatter
直接放代码吧
> 我遇到的struct都是C的API,所以没有太复杂的类型。
1template<int T>
2struct std::formatter<int[T]> {
3 constexpr auto parse(std::format_parse_context &ctx) {
4 return ctx.begin();
5 }
6
7 template<typename FormatContext>
8 auto format(const int p[T], FormatContext &ctx) const {
9 ctx.out() = std::format_to(ctx.out(), "int[{}]{{", T);
10 for (int i = 0; i < T; ++i) {
11 if (i == T - 1) {
12 ctx.out() = std::format_to(ctx.out(), "{}", p[i]);
13 } else {
14 ctx.out() = std::format_to(ctx.out(), "{}, ", p[i]);
15 }
16 }
17 ctx.out() = std::format_to(ctx.out(), "}}");
18 return ctx.out();
19 }
20
21};
可以将int[]数组格式化掉。
struct的format函数
这里把formatter的parse函数单独实现为一个模板函数。
1template<typename S, typename FormatContext>
2auto my_struct_format(const S &p, FormatContext &ctx) {
3 constexpr auto struct_name = ylt::reflection::type_string<S>();
4 ctx.out() = std::format_to(ctx.out(), "{}{{ ", struct_name);
5 constexpr auto sz = ylt::reflection::members_count<S>();
6 ylt::reflection::for_each(p, [&ctx](auto &field, auto name, auto index) {
7 if (index < sz - 1) {
8 ctx.out() = std::format_to(ctx.out(), "{}: {}, ", name, field);
9 } else {
10 ctx.out() = std::format_to(ctx.out(), "{}: {} }}", name, field);
11 }
12 });
13 return ctx.out();
14}
首先获取struct的名字,获取结构体成员个数。
再利用1ylt::reflection::for_each
遍历每个成员和每个值。特判是否是最后一个成员,处理1,
。
enum的format函数
enum没有出现在官方例子里, 我这瞎搞了半天,看样子是对的 。
直接贴一下关键API1get_enum_arr
的实现吧
1// Enumerate the numbers in a integer sequence to see if they are legal enum
2// value
3template <typename E, std::int64_t... Is>
4constexpr inline auto get_enum_arr(
5 const std::integer_sequence<std::int64_t, Is...> &) {
6 constexpr std::size_t N = sizeof...(Is);
7 std::array<std::string_view, N> enum_names = {};
8 std::array<E, N> enum_values = {};
9 std::size_t num = 0;
10 (([&]() {
11 constexpr auto res = try_get_enum_name<E, static_cast<E>(Is)>();
12 if constexpr (res.first) {
13 // the Is is a valid enum value
14 enum_names[num] = res.second;
15 enum_values[num] = static_cast<E>(Is);
16 ++num;
17 }
18 })(),
19 ...);
20 return std::make_tuple(num, enum_values, enum_names);
21}
三个返回值:
- num: enum的成员个数
- enum_values:每个成员的值,是一个数组,大小是可变模板参数Is的多少。
- enum_names: 每个成员的名称,是一个数组,大小同上。
这个函数要求传入定长的integer sequence,一般情况enum的枚举个数不一定一样。我这里直接传个20,假设我要格式化的enum的枚举个数都不超过20。看上边这个代码,20这个参数用来申请内存空间的,多了无所谓,预留默认空值。
这样,enum的格式化函数如下。
1template<typename S, typename FormatContext>
2auto my_enum_format(const S &p, FormatContext &ctx) {
3 constexpr auto index_enum_str = ylt::reflection::get_enum_arr<S>(std::make_integer_sequence<int64_t, 20>{});
4 constexpr auto enum_name = ylt::reflection::type_string<S>();
5 for (int i = 0; i < 20; ++i) {
6 if (std::get<1>(index_enum_str)[i] == p) {
7 ctx.out() = std::format_to(
8 ctx.out(),
9 "{}::{}(value = {})",
10 // 这个p别忘了强制转为p,不然无限递归了
11 enum_name, std::get<2>(index_enum_str)[i], (int) p
12 );
13 break;
14 }
15 }
16 return ctx.out();
17}
测试
1struct S1 {
2 int a;
3 float b;
4 int c[6];
5 float d[3];
6};
7
8enum E1 {
9 XX = 10,
10 YY,
11 MAX
12};
13
14int main() {
15 S1 s{.a = 42, .b = 10.24, .c = {0, 1, 4, 9, 16, 25}, .d = {1.1, 2.2, 3.3}};
16 std::cout << std::format("s = {}\n", s);
17 E1 y = E1::MAX;
18 std::cout << std::format("y = {}", y);
19 return 0;
20}
输出结果如下
1s = S1{ a: 42, b: 10.24, c: int[6]{0, 1, 4, 9, 16, 25}, d: float[3]{1.1, 2.2, 3.3} }
2y = E1::MAX(value = 12)
所有代码
1cmake_minimum_required(VERSION 3.15)
2project(test_formatter)
3
4set(CMAKE_CXX_STANDARD 20)
5
6
7include(FetchContent)
8
9FetchContent_Declare(
10 yalantinglibs
11 GIT_REPOSITORY https://github.com/alibaba/yalantinglibs.git
12 GIT_TAG "0.3.5" # optional ( default master / main )
13 GIT_SHALLOW 1 # optional ( --depth=1 )
14)
15
16FetchContent_MakeAvailable(yalantinglibs)
17
18
19add_executable(${PROJECT_NAME} main.cpp)
20target_link_libraries(${PROJECT_NAME} PRIVATE yalantinglibs::yalantinglibs)
21target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_20)
后
小坑小点还是挺多的。比如std::formatter的特化没法放到某个namespace里。即
1namespace test_space{
2 struct S{};
3 templace<>
4 struct std::formatter<S>{
5 // ...
6 };
7}
会编译失败。
虽然达到我想要的结果了,但是有几个点感觉不是清晰。
- int[T]的格式化,能再写成个模板好了,可以迭代的容器的格式化。不知道view行不行。
- enum的格式化,感觉应该有个api获取enum的格式,在编译期计算,这样传给get_enum_arr就不会多余或者不够了。应该是我没找到相关内容。
- 每个struct都要写一个宏。
1MY_STRUCT_FORMAT(S1);
1MY_ENUM_FORMAT(E1);
这样。
唉,不是不能用。