Posts
Materials
EN
Gray Wood
Posts
Materials
EN
Gray Wood
2023-03-09

C++ 编译期反射读写

C++
environments
C++: C++20

前言

最近发现 refl-cpp 库可以实现编译期计算反射读写的结果,比较好奇内部的实现。因此在浅略地学习内部实现后,笔者实现了一个简单反射库,主要目的是学习 C++ 模板编程。

这个库支持以下功能:

  • Reflect::get(...) 反射读结构体成员属性
  • Reflect::set(...) 反射写结构体成员属性
  • forEach<T>(...) 遍历结构体成员

完整的代码放在了 这里,以下是使用的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct Point {
  float x;
  float y;
  float z;
};

REFLECT_BEGIN(Point)
REFLECT_FIELD(x)
REFLECT_FIELD(y)
REFLECT_FIELD(z)
REFLECT_END

void example() {
  Point pt{.x = 1, .y = 2, .z = 3};
  std::cout << "get(x):" << Reflect::get(pt, TypeInfo<Point>::x()) << std::endl;

  Reflect::set(pt, TypeInfo<Point>::y(), 4);
  std::cout << "after set y=4:" << pt.y << std::endl;

  std::cout << "foreach:" << std::endl;
  Reflect::forEach<Point>([&](auto &&member) {
    std::cout << member.name.data() << ":" << Reflect::get(pt, member)
              << std::endl;
  });

  // get(x):1
  // after set y=4:4
  // foreach:
  // x:1
  // y:4
  // z:3
}

int main() { example(); }

编译期计算的字符串 ConstStr

在元信息中需要存储成员属性的名称,需要使用可以编译期进行表达、字符串连接、字符串判等的结构。以下定义了 ConstStr,其通过字符串常量初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <int SIZE> struct ConstStr {
  static constexpr int LEN = SIZE - 1;
  constexpr explicit ConstStr() : _s{} {}
  // "const char (&s)[SIZE]" 表示 "const char s[SIZE]" 的引用
  constexpr explicit ConstStr(const char (&s)[SIZE])
      : ConstStr(s, std::make_index_sequence<SIZE>()) {}

private:
  template <size_t... Idx>
  constexpr explicit ConstStr(const char (&s)[SIZE],
                              std::index_sequence<Idx...>)
      : _s{s[Idx]...} {}

  char _s[SIZE];
};

以下补充一些辅助方法:

1
2
3
4
5
template <int SIZE> struct ConstStr {
  constexpr size_t size() const { return LEN; }
  constexpr inline auto data() const { return _s; }
  constexpr inline auto data() { return _s; }
};

再实现字符串的相连与判等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
template <int N, int M>
constexpr bool operator==(const ConstStr<N> &a, const ConstStr<M> &b) noexcept {
  if constexpr (N != M) {
    return false;
  } else {
    // 最后是 \0,可以跳过对比
    for (size_t i = 0; i < M - 1; i++) {
      if (a.data()[i] != b.data()[i]) {
        return false;
      }
    }
    return true;
  }
}

template <int N, int M>
constexpr ConstStr<N + M> operator+(const ConstStr<N> &a,
                                    const ConstStr<M> &b) noexcept {
  auto s = ConstStr<N + M>();
  for (auto i = 0; i < N; i++) {
    s.data()[i] = a.data()[i];
  }
  for (auto j = 0; j < M; j++) {
    s.data()[N + j] = b.data()[j];
  }
  return s;
}

元信息存储

假定需要支持结构体 Point { float x; float y; float z; } 的反射读写能力,以属性 x 为例,使用 traits 技巧记录以下信息:

  • 成员类型
  • 成员名,使用 ConstStr 表达
  • 成员指针,当有数据时可获取成员值

对于整一个结构,需要记录:

  • 成员数量

以下使用 Member<Idx> 是为了后续能够使用宏生成,且能够遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <typename T> struct TypeInfo;
template <> struct TypeInfo<Point> {
  typedef Point ClassType;
  template <size_t Idx> struct Member {};

  template <> struct Member<0> {
  public:
    // 成员类型
    typedef decltype(ClassType::x) MemberType;
    // 成员名
    static constexpr auto name = ConstStr("x");
    // 成员指针
    static constexpr auto pointer = &ClassType::x;
  };
  // 便于构造属性 x 的元信息
  static constexpr Member<0> x() { return {}; }

  // 成员数量
  static constexpr size_t memberCount = 1;
};

反射读写

有了元信息的读取结构就可以实现反射读写方法。

1
2
3
4
5
6
7
8
9
10
11
12
// 读方法,如 get(pt, TypeInfo<Point>::x())
template <typename C, typename M> constexpr static auto get(const C &obj, M) {
  constexpr auto pointer = M::pointer;
  return obj.*pointer;
}

// 写方法,如 set(pt, TypeInfo<Point>::x(), 1.f)
template <typename C, typename M>
constexpr static void set(C &obj, M, const typename M::MemberType &val) {
  constexpr auto pointer = M::pointer;
  obj.*pointer = val;
}

forEach

定义空的结构体 TypeList 用于承载元信息列表。

1
template <typename... T> struct TypeList {};

由于成员元信息使用 Member<Idx> 记录,同时已知成员数量 memberCount,因此可以使用 std::index_sequence 获取成员列表。

1
2
3
4
5
6
7
8
9
template <typename C> constexpr static auto getMembers() {
  constexpr auto seq = std::make_index_sequence<TypeInfo<C>::memberCount>();
  return getMembers<C>(seq);
}
template <typename C, size_t... Idx>
constexpr static TypeList<Member<C, Idx>...>
getMembers(std::index_sequence<Idx...>) {
  return {};
}

这样就可以通过已获取的成员列表实现遍历方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename C, typename F> constexpr static void forEach(F &&f) {
  forEach(getMembers<C>(), std::forward<F>(f));
}

template <typename F, typename... Ts>
constexpr static void forEach(TypeList<Ts...>, F &&f) {
  forEach(TypeList<Ts...>{}, std::make_index_sequence<sizeof...(Ts)>(),
          std::forward<F>(f));
}
template <typename F, typename... Ts, typename T, size_t... Idx, size_t I>
constexpr static void forEach(TypeList<T, Ts...>,
                              std::index_sequence<I, Idx...>, F &&f) {
  // 生成 T 类型对象并调用 callback
  f(T{});
  forEach(TypeList<Ts...>{}, std::index_sequence<Idx...>{},
          std::forward<F>(f));
}
template <typename F>
constexpr static void forEach(TypeList<>, std::index_sequence<>, F &&) {}

以下是对 Point 使用 forEach 的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 遍历全部属性
void example_iterateAllProps() {
  Point pt{.x = 1, .y = 2, .z = 3};
  Reflect::forEach<Point>([&](auto member) {
    std::cout << member.name.data() << ":" << Reflect::get(pt, member)
              << std::endl;
  });

  // Output:
  // x:1
  // y:2
  // z:3
}

// 遍历部分属性
void example_iterateSomeProps() {
  typedef TypeInfo<Point> T;
  // 将需要的属性组成 TypeList
  constexpr TypeList<decltype(T::x()), decltype(T::z())> props;
  Point pt{.x = 1, .y = 2, .z = 3};
  Reflect::forEach(props, [&](auto member) {
    std::cout << member.name.data() << ":" << Reflect::get(pt, member)
              << std::endl;
  });

  // Output:
  // x:1
  // z:3
}

宏生成元信息

将之前手写 TypeInfoMember 的地方转为宏生成以方便使用。宏的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define REFLECT_BEGIN(Type)                                                    \
  template <> struct TypeInfo<Type> {                                          \
    typedef Type ClassType;                                                    \
    template <size_t Idx> struct Member {};                                    \
    static constexpr size_t fieldOffset = __COUNTER__;

#define REFLECT_FIELD(Field)                                                   \
  static constexpr size_t MEMBER_##Field##_OFFSET = __COUNTER__;               \
  template <> struct Member<MEMBER_##Field##_OFFSET - fieldOffset - 1> {       \
  public:                                                                      \
    typedef decltype(ClassType::Field) MemberType;                             \
    static constexpr auto name = ConstStr(#Field);                             \
    static constexpr auto pointer = &ClassType::Field;                         \
  };                                                                           \
  static constexpr Member<MEMBER_##Field##_OFFSET - fieldOffset - 1> Field() { \
    return {};                                                                 \
  }

#define REFLECT_END                                                            \
  static constexpr size_t memberCount = __COUNTER__ - fieldOffset - 1;         \
  };

其中的关键是 Member<Idx>memberCount 如何生成的问题。这里用到了内置宏 __COUNTER__,其初值为 0,每预编译一次会自增 1。那么在开始时记录下其值,最后再记录一次就能够算出成员数量,每次定义成员元信息记录其值能够算出自增值。

Point 使用宏生成元数据信息:

1
2
3
4
5
REFLECT_BEGIN(Point)
  REFLECT_FIELD(x)
  REFLECT_FIELD(y)
  REFLECT_FIELD(z)
REFLECT_END