使用yalantinglibs中的反射库格式化自定义struct/enum


2024-07-20

目录

std::format的基本用法 ⁠1
基本用法 ⁠1
扩展用法 ⁠1
其他用法 ⁠1
yalantinglibs的reflection ⁠1
几个api ⁠1
int[]的formatter ⁠1
struct的format函数 ⁠1
enum的format函数 ⁠1
测试 ⁠1
所有代码 ⁠1
⁠1

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

  1. 1constexpr auto sz = ylt::reflection::members_count<S>();获取S的成员个数。编译期计算。
  2. 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) {}
  3. 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)

main.cpp pastebin

小坑小点还是挺多的。比如std::formatter的特化没法放到某个namespace里。即

1namespace test_space{
2 struct S{};
3 templace<>
4 struct std::formatter<S>{
5 // ...
6 };
7}

会编译失败。

虽然达到我想要的结果了,但是有几个点感觉不是清晰。

  1. int[T]的格式化,能再写成个模板好了,可以迭代的容器的格式化。不知道view行不行。
  2. enum的格式化,感觉应该有个api获取enum的格式,在编译期计算,这样传给get_enum_arr就不会多余或者不够了。应该是我没找到相关内容。
  3. 每个struct都要写一个宏。1MY_STRUCT_FORMAT(S1); 1MY_ENUM_FORMAT(E1);这样。

唉,不是不能用。