查看: 106|回复: 8

一个极简的 C++ 静态反射 demo

[复制链接]

2

主题

5

帖子

9

积分

新手上路

Rank: 1

积分
9
发表于 2022-12-21 14:41:38 | 显示全部楼层 |阅读模式
感谢某同事手把手教会我写这个 demo
下面这个类可以静态枚举字段:
struct A : Base {
    ADD_FIELD(int, a, 110);
    ADD_FIELD(double, b, 1.2);
    ADD_FIELD(std::string, c, "OK");
    ADD_FIELD(uint32_t, d, 27);
    std::string others;
};

int main() {
    A a;
    Helper::visit([](std::string_view name, auto &&value) {
        std::print("name: {} value: {}\n", name, value);
    }, a);

    Helper::apply([](int a, double b, std::string_view c, uint32_t d) {
        std::print("a: {} b: {} c: {} d: {}\n", a, b, c, d);
    }, a);
}
实现静态反射的难点在于如何保存不定数量的类型信息。如果我们能知道

  • 有多少个 field
  • 第 I 个 field 是什么类型
就能进一步实现出这个 demo 了。这个思路,看起来是不是很像向一个 list 中 append 一个个类型?我们不一定要真的维护一个 type list,我们可以想办法每次 ADD_FIELD 时得到当前的 list 长度,从而确定这个 field 的索引编号。
进一步地,我们可以将“得到 list 长度”这个操作转化为“得到当前最大索引编号”。
这里用到的主要技巧:函数重载决议的时候,如果没有完美匹配实参的函数,编译器会选择能将实参隐式转换到形参的函数。
比如形参是实参的基类:
struct Base {};
struct Derived : Base {};

void f(Base);

f(Derived{}); // f(Base) 会被选中
进一步地,如果多个重载函数的形参都是实参的基类,则距离实参继承关系最近的基类版本会被选中:
struct A {};
struct B : A {};

void f(A);
void f(B);

f(C{}); // f(B) 会被选中
那么如果我们维护一个继承链,对应一组重载函数,其中每个类型对应一个形参版本,我们能做到什么呢?
我们可以知道这个函数有多少个重载:
template <size_t N>
struct Rank : Rank<N - 1> {
    static constexpr auto rank = N;
};

template <>
struct Rank<0> {
    static constexpr auto rank = 0;
};
假设我们人肉定义了以下重载:

  • Rank<0> f(Rank<0>)
  • Rank<1> f(Rank<1>)
  • ...
  • Rank<50> f(Rank<50>)
则我们用一个非常大的 Rank 就可以知道当前有多少个 f:
std::cout << decltype(f(Rank<100>{}))::rank << std::endl; // 50
进一步地,我们还能用某个常数索引取出对应的 Rank:
std::cout << decltype(f(Rank<20>{}))::rank << std::endl; // 20
回到文首的例子,如果我们能将每个 field 的类型作为一个重载函数的返回值,就可以用索引来得到对应 field 的类型了。
人肉写出来大约是这样:
int f(Rank<0>);
double f(Rank<1>);
std::string f(Rank<2>);
uint32_t f(Rank<3>);
抽象化大概长这样:
ADD_FIELD(T, name) T f(Rank<?>)
这里的问题在于 ? 怎么生成。我们想通过某种方式,生成一个整数序列,听起来是不是很递归?但函数声明怎么递归呢?
看起来,我们需要每声明一个 f 时从上一个 f 获得帮助递归的信息:
T f(Rank<current_max_rank + 1>);
那 current_max_rank 该怎么获取呢?从前面的例子中我们知道,我们可以用一个继承链末端的派生类来触发重载决议,从而得到当前 rank 最大的 f 的返回类型。因此我们还需要在返回类型中加上 rank 信息:
template <typename T, size_t N>
struct TypeInfo {
    using type = T;
    static constexpr auto rank = N;
};

#define CURRENT_MAX_RANK decltype(f(Rank<100>{}))::rank
#define NEXT_RANK (CURRENT_MAX_RANK + 1)

TypeInfo<T, NEXT_RANK> f(Rank<NEXT_RANK>);
这样我们每定义一个 field,就自动得到了一个具有更大 rank 的 f,其返回类型中就包含着我们要的信息。
接下来,我们需要为递归设置一个终点:
Rank<0> f(Rank<0>);
合起来,就是下面的代码啦:
struct Base {
    static Rank<0> f(Rank<0>);
};

// 实际上 ADD_FIELD 里的 NEXT_RANK 宏很可能不展开,导致编译失败,不过不重要
#define ADD_FIELD(Type, name, ...) \
    Type name{__VA_ARGS__}; /* 用可选参数初始化 */\
    static TypeInfo<Type, NEXT_RANK> f(Rank<NEXT_RANK>)

struct A : Base {
    ...
};
然后我们就可以利用这些信息枚举 A 中的每个 field 类型了:
template <typename T, size_t I>
using FieldType = std::decay_t<decltype(T::f(Rank<I>{}))>;

// FieldType<A, 1> -> int
// FieldType<A, 2> -> double
// FieldType<A, 3> -> std::string
// FieldType<A, 4> -> uint32_t
还能按顺序遍历 A 的每个字段:
template <typename F, typename T, size_t I = 1>
void visit(F && f, T && t) {
    if constexpr (I <= MaxRank<T>) {
        f(FieldType<T, I>::?);
        visit<F, T, I + 1>(std::forward<F>(f), std::forward<T>(t));
    }
}
这里我们遇到的问题是:如何在遍历过程中拿到每个 field 的值。
我们可以在 TypeInfo 中增加一个 getter:
template <typename T, size_t N, auto Getter>
struct TypeInfo {
    using type = T;
    static constexpr auto rank = N;
    static constexpr auto getter = Getter;
};

#define ADD_FIELD(Type, name, ...) \
    Type name{__VA_ARGS__}; /* 用可选参数初始化 */\
    static TypeInfo<Type, NEXT_RANK, &T::name> f(Rank<NEXT_RANK>)
注意这里我们获取的是成员变量指针,需要配合对象一起使用。接下来修改 visit 中调用 f 的地方:
using FT = FieldType<T, I>;
    f(t.*FT::getter);
这样我们就拿到了每个 field 的值。
接下来,我们还想拿 field name。可是字符串怎么放进 TypeInfo 中呢?std::string 和 std::string_view 都不能作为模板参数,那我们就将它转成字符数组:
template <size_t N>
struct NameWrapper {
  constexpr NameWrapper(const char(&str)[N]) { std::copy_n(str, N, string); }
  constexpr operator std::string_view() const { return {string, N - 1}; }
  char string[N];
};

template <NameWrapper Name, ...>
struct TypeHelper {
  static constexpr std::string_view name = Name;
  ...
};
不太冷的冷知识:字符数组可以作为常量存在。
于是上面的宏定义还得改:
#define ADD_FIELD(Type, name, ...) \
    Type name{__VA_ARGS__}; /* 用可选参数初始化 */\
    static TypeInfo<NameWrapper(#name), Type, NEXT_RANK, &T::name> f(Rank<NEXT_RANK>)
再改 visit:
f(FT::name, a.*FT::getter);
终于,我们完成了 visit,还差个 apply。不分析了,直接给答案:
template <class T, size_t I>
    requires (I > 0 && I <= MaxRank<T>)
auto getValue(T &a) {
    using Type = FieldType<T, I>;
    return a.*Type::getter;
}

template <typename F, typename T, std::size_t... I>
auto applyImpl(F&& f, T& t, std::index_sequence<I...>) {
    return f(getValue<T, I + 1>(t)...); // I + 1 确保序列值为 [1, ..., N]
}

template <typename F, typename T>
auto apply(F &&f, T &t) {
    return applyImpl(std::forward<F>(f), t, std::make_index_sequence<MaxRank<T>>{});
}
这里用到的知识点:

  • fold expression
  • requires
  • integer sequence
以上基本照搬 apply 的实现。
由此,我们终于完成了这个极简的静态反射的 demo。
2022-11-11 更新:
上面的例子实际可以拆成两部分,一部分是类型索引功能,比较通用,不需要看到真正的 TypeInfo;另一部分是根据索引出来的类型做运算。
类型索引:
template <size_t N>
struct Rank : Rank<N - 1> {
    static constexpr auto rank = N;
};

template <>
struct Rank<0> {
    static constexpr auto rank = 0;
};

template <typename T, size_t I>
using FieldType = std::decay_t<decltype(T::f(Rank<I>{}))>;

template <typename T>
inline constexpr auto MaxRank = std::decay_t<decltype(T::f(Rank<100>{}))>::rank;

template <typename T, typename F, std::size_t... I>
auto applyImpl(F&& f, std::index_sequence<I...>) {
    return f(FieldType<T, I + 1>{}...); // I + 1 确保序列值为 [1, ..., N]
}

// f 的参数是一系列用于传递类型信息的空对象
template <typename T, typename F>
auto applyBase(F &&f) {
    return applyImpl<T>(std::forward<F>(f), std::make_index_sequence<MaxRank<T>>{});
}
静态反射:
template <size_t N>
struct NameWrapper {
  constexpr NameWrapper(const char(&str)[N]) { std::copy_n(str, N, string); }
  constexpr operator std::string_view() const { return {string, N - 1}; }
  char string[N];
};

template <NameWrapper Name, typename T, size_t I, auto Getter>
struct TypeInfo {
  using type = T;
  static constexpr std::string_view name = Name;
  static constexpr auto getter = Getter;
  static constexpr auto rank = I;
};

template <typename T>
struct Base {
    using Self = T;
    static Rank<0> f(Rank<0>);
};

#define ADD_FIELD(Type, name, ...) \
    Type name{__VA_ARGS__}; \
    static TypeInfo<NameWrapper(#name), Type, decltype(f(Rank<100>{}))::rank + 1, &Self::name> f(Rank<decltype(f(Rank<100>{}))::rank + 1>)

template <typename F, typename T>
auto visit(F &&f, T &t) {
  auto visitor = [&](auto &&arg) {
    using Type = std::decay_t<decltype(arg)>;
    f(Type::name, t.*Type::getter);
  };
  auto iterate = [&](auto &&...args) {
    (visitor(std::forward<decltype(args)>(args)),...);
  };
  return applyBase<T>(std::move(iterate));
}
demo:
struct A : Base<A> {
  ADD_FIELD(int, a, 110);
  ADD_FIELD(double, b, 1.2);
  ADD_FIELD(std::string, c, "OK");
  ADD_FIELD(uint32_t, d, 27);
  std::string others;
};

int main() {
  A a;

  visit([](std::string_view name, auto &&value) {
    std::cout << "name: " << name << " value: " << value << std::endl;
  }, a);
}
完整实现:Compiler Explorer
回复

使用道具 举报

3

主题

7

帖子

13

积分

新手上路

Rank: 1

积分
13
发表于 2022-12-21 14:42:30 | 显示全部楼层
template <typename T, size_t I>using FieldType = std::decay_t<decltype(T::f(Rank<100>{}))>;抓个虫
回复

使用道具 举报

2

主题

9

帖子

10

积分

新手上路

Rank: 1

积分
10
发表于 2022-12-21 14:42:49 | 显示全部楼层
细说
回复

使用道具 举报

2

主题

7

帖子

10

积分

新手上路

Rank: 1

积分
10
发表于 2022-12-21 14:42:57 | 显示全部楼层
你这个参数I咋没用呢,这里是需要index不是找max吧
回复

使用道具 举报

3

主题

7

帖子

13

积分

新手上路

Rank: 1

积分
13
发表于 2022-12-21 14:43:56 | 显示全部楼层
哦哦哦,没反应过来,确实
回复

使用道具 举报

5

主题

9

帖子

18

积分

新手上路

Rank: 1

积分
18
发表于 2022-12-21 14:44:33 | 显示全部楼层
fixed
回复

使用道具 举报

4

主题

10

帖子

18

积分

新手上路

Rank: 1

积分
18
发表于 2022-12-21 14:44:40 | 显示全部楼层
boost::pfr ?
回复

使用道具 举报

4

主题

12

帖子

20

积分

新手上路

Rank: 1

积分
20
发表于 2022-12-21 14:45:36 | 显示全部楼层
有那个意思,主要体验下造轮子 [酷]
回复

使用道具 举报

2

主题

6

帖子

10

积分

新手上路

Rank: 1

积分
10
发表于 2022-12-21 14:46:32 | 显示全部楼层
有朋友找工作吗?需要一位喜欢C++的同学(Windows平台)
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表