第五章:编译期反射初探!

在前面,我们提到了除了标签分发,还可以通过编译期反射来判断类的成员有没有某个类型。

反射是程序在运行时或编译期能够**“自我检查、自我操作”的能力**。

程序可以“知道自己有哪些类、成员变量、类型信息”,甚至动态地访问和修改它们

编译期反射:在编译期间就可以分析、枚举、获取类型的成员信息。

C++26 正式加入反射:

1
2
3
4
5
6
7
8
9
10
11
12
#include <meta>

struct A { int x; double y; };

consteval void analyze() {
using namespace std::meta;
auto info = reflexpr(A); // 反射出类型信息
auto members = members_of(info); // 获取所有成员
for (auto m : members) {
std::println("Member: {}", name_of(m)); // 打印每个成员的名字
}
}

目前常见的编译期反射库基本都是侵入式的,就是在类内定义一个宏来注册一些方法帮助反射,本章带领大家实现一个简单的非侵入式的编译期反射。
我们的目标是让如下代码成功编译并通过测试:

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

struct Dog
{
bool m_male;
};

struct Person
{
reaction::Field<std::string> m_name;
reaction::Field<int> m_age;
bool m_male;
};

class PersonPrivate {
reaction::Field<std::string> m_name;
reaction::Field<int> m_age;
bool m_male;
};
REFL_PRIVATE(PersonPrivate, &PersonPrivate::m_name, &PersonPrivate::m_age, &PersonPrivate::m_male)

TEST(BasicTest, FieldTest) {
static_assert(!reaction::reflectField_v<Dog>);
static_assert(reaction::reflectField_v<Person>);
static_assert(reaction::reflectField_v<PersonPrivate>);
[&]<size_t... Is>(std::index_sequence<Is...>) {
(std::cout << ... << get_member_name<std::get<Is>(tp)>());
}(std::make_index_sequence<std::tuple_size_v<decltype(tp)>>{});
}

我们需要分别对aggregate类型和非聚合类型反射:

在 C++ 中,PODTrivialAggregate 类型虽然有重叠,但它们有不同的定义侧重点,理解这些概念对于深入掌握类型系统、内存布局和构造行为非常重要。下面我通过定义 + 举例 + 差异说明来讲解这三者的区别。

1. POD(Plain Old Data)类型

定义:满足以下两个条件的类型:

  • 是 Trivial 类型(平凡构造/析构/复制/移动)
  • 是 Standard Layout 类型(布局规则简单,兼容 C)

📌 用途:可与 C 语言数据结构兼容(可 memcpy 拷贝等)

2. Trivial 类型

定义:具有以下特征的类型:

  • 构造函数、析构函数是编译器默认生成的(且没被用户定义)
  • 拷贝/移动构造和赋值操作符也必须是默认的
  • 没有虚函数、没有虚基类

📌 用途:构造/析构开销为零,可 memset 初始化

3. Aggregate 类型

定义:聚合类型(C++17 后有所放宽),满足以下条件:

  • 没有用户提供的构造函数(注意默认构造函数是 OK 的)
  • 没有 private/protected 的非静态成员
  • 没有基类
  • 没有虚函数表(vtable)

📌 用途:可以使用大括号 {} 初始化所有成员(aggregate initialization)

Standard-layout(标准布局类型)是 C++ 中一个与内存布局紧密相关的类型特性,用于确保类型的成员排列方式在不同编译器中行为一致,便于进行底层操作,如 memcpy、C/C++ 混合编程等。

Standard-layout 类型定义

一个类型是 standard-layout,必须满足以下条件(C++11 起):

  1. 所有非静态成员的访问权限必须一致(都为 publicprotectedprivate);
  2. 没有虚函数;
  3. 没有虚基类;
  4. 所有非静态成员都必须是标准布局类型;
  5. 所有基类(如果有)都是标准布局类型;
  6. 至多有一个基类;
  7. 派生类中第一个非静态成员与其第一个非静态基类不能是同一类型(避免二义性);
  8. 所有非静态数据成员必须在其自身类或基类中声明(不能跨类出现歧义);
  9. 不使用空基类优化(Empty Base Optimization, EBO);

📌 重点目的:使类型的内存布局“可预测、紧凑、无重排”。


在完成这个浩大的工程之前,让我们先看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

#include <iostream>
#include <string>

int global_a = 10;

template <auto val>
inline constexpr std::string_view getFunName() {
#if defined(__GNUC__)
constexpr std::string_view func_name = __PRETTY_FUNCTION__;
#elif defined(_MSC_VER)
constexpr std::string_view func_name = __FUNCSIG__;
#endif
return func_name;
}

int main()
{
// int a = 10;
auto name = getFunName<&global_a>();
std:: cout << name << std::endl;
}

🌟 非类型模板参数(Non-Type Template Parameter, NTTP)

C++ 模板的非类型参数允许你在编译时提供具体的值,比如常量、指针、枚举、成员指针等,以此增强模板的灵活性。

🧾 基本语法

1
2
3
4
5
template <typename T, int N>
class Array {
public:
T arr[N]; // 数组大小为 N
};

这里,N 是非类型模板参数,用于指定数组大小。


🧪 使用示例

1. 常量整数

1
2
3
4
5
6
7
8
9
template <int N>
void printN() {
std::cout << "N = " << N << std::endl;
}

int main() {
printN<10>(); // 输出 N = 10
printN<20>(); // 输出 N = 20
}

2. 指针或引用

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T, T* ptr>
class PointerWrapper {
public:
void printPointer() {
std::cout << "Pointer address: " << ptr << std::endl;
}
};

int main() {
int x = 10;
PointerWrapper<int, &x> pw;
pw.printPointer();
}

3. 枚举类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum class Color { Red, Green, Blue };

template <Color c>
void printColor() {
if constexpr (c == Color::Red) {
std::cout << "Red\n";
} else if constexpr (c == Color::Green) {
std::cout << "Green\n";
} else {
std::cout << "Blue\n";
}
}

int main() {
printColor<Color::Red>();
printColor<Color::Green>();
}

我们可以借助结构体绑定获取所有成员的引用:

1
auto&& [m1, m2, m3] = Person{};

然后我们把这三个元素打包成一个tuple遍历不就行了:

1
2
auto&& [m1, m2, m3] = Person{};
auto ref_tup = std::tie(m1, m2, m3);

问题是非类型参数必须是编译期常量,如果是指针类型指针指向的地址必须是编译期确定的,显然Person{}是一个运行期的值,所以ref_tup也是一个运行期的tuple,那么我们如何获取一个编译期的Person呢?

全局变量由于它的地址是编译时已知的,所以它可以被当成一个编译期常量:

1
2
3
4
5
6
7
8
Person g_p;

int main() {
Person p;
constexpr auto&& ref = g_p;
constexpr auto&& ref2 = p; //false
}

但是我们不可能为每个要反射的类型写一个全局变量,我们可以用一个全局变量模板封装一下:

1
2
3
4
5
6
7
8
template<typename T>
constexpr auto g_value = T{};

int main() {
auto&& [m1, m2, m3] = g_value<Person>;
constexpr auto ref_tup = std::tie(m1, m2, m3); //(这里msvc居然编译不通过,坑啊)
}

这里还有个更好的写法,就是inline static静态成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

template <class T>
struct my_wrapper {
inline static T value;
};

template <class T>
inline constexpr T& get_global_object() noexcept {
return my_wrapper<T>::value;
}

int main() {
auto&& [m1, m2, m3] = get_global_object<Person>();
constexpr auto ref_tup = std::tie(m1, m2, m3);
}

  • inline static 成员变量在 C++17 之后是隐式初始化的(默认初始化或零初始化),并且它的生命周期是整个程序的运行时间(静态存储期)。
  • 由于它是一个全局唯一的实例,它的地址在编译时是已知的(即使它的值可能在运行时初始化)

这里的inline是必须的,否则value就只有声明,没有定义,违反ODR原则。
ODR(One Definition Rule)是 C++ 中的一条规则,它要求一个类型、函数或者变量在整个程序中只能有一个定义。

至此,我们已经成功一大半了,剩下的工作就是用元编程技术对这个tuple遍历,打印所有的成员名称,并判断是否存在Field类型了:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

template <typename T>
struct Is_Field : std::false_type {
};

template <typename T>
struct Is_Field<Field<T>> : std::true_type {};

template <typename Tuple>
constexpr bool check_field(const Tuple& tuple) {
bool found = false;

std::apply([&](const auto&... args) {
((found = found || Is_Field<std::decay_t<decltype(args)>>()), ...);
}, tuple);

return found;
}

template <class T>
struct my_wrapper {
inline static T value;
};

template <class T>
inline constexpr T& get_global_object() noexcept {
return my_wrapper<T>::value;
}

template <typename T>
constexpr auto getTuple() {
auto& [member1, member2, member3] = g_value<Person>;
constexpr auto ref_tup = std::tie(member1, member2, member3);
}

template <typename T>
constexpr auto reflectField() {
constexpr auto ref_tup = getTuple<T>();
return check_field(ref_tup);
}

int main() {
static_assert(reflectField<Person>);
constexpr auto tp = getTuple<Person>();
[&]<size_t... Is>(std::index_sequence<Is...>) {
(std::cout << ... << getFunName<&std::get<Is>(tp)>());
}(std::make_index_sequence<std::tuple_size_v<decltype(tp)>>{});
}

问题来了,我们在结构化绑定的时候是手动指定了三个成员,那么如何适配N个成员?

首先我们要在编译期计算类型有多少个成员?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

struct AnyType {
template <typename T>
operator T();
};

template <typename T>
consteval size_t countMember(auto&&... Args) {
if constexpr (!requires { T{Args...}; }) {
return sizeof...(Args) - 1;
}
else {
return countMember<T>(Args..., AnyType{});
}
}

template <typename T>
constexpr size_t member_count_v = countMember<T>();

AnyType的转换函数只需声明就行,requires检测也就是SFINAE技术是在模板参数替换阶段(也就是编译器尝试具体化模板时)进行语法层面的“合法性检查”,而不是运行或完整编译代码。

requiresdeclval<T> 的共性:编译时检查与“伪实例化”

requiresdeclval<T> 的共同点在于它们均用于 编译时 的类型或表达式合法性检查,且均 不会生成实际运行时代码。它们的核心目的是让编译器在模板元编程或概念约束中推导类型的可行性,而非实际构造或操作对象。


declval<T> 的“伪实例化”特性

declval<T> 是标准库中的一个工具函数模板,其定义大致如下:

1
2
3
4
5
6
7
8
9
template <typename T>
add_rvalue_reference_t<T> declval() noexcept;

struct Foo {
int value;
};

// 推导 Foo 的成员 value 的类型
using ValueType = decltype(std::declval<Foo>().value); // ValueType = int

它的作用是 在编译时类型推导的上下文中,生成一个 T 的右值引用。关键点在于:

  • 不调用构造函数declval<T>() 不会实际构造 T 的实例,甚至不要求 T 可默认构造或有可访问的构造函数。
  • 未求值上下文declval<T> 通常用于 decltypesizeof 或模板参数推导等 未求值上下文(unevaluated contexts),编译器只需知道类型信息,无需生成实际代码。

这里使用了模板递归,AnyType可以转换为任意类型,一直用requires检测T构造函数的合法性:

T{}, T{any}, T{any1, any2}, T{any1, any2, any3},直到失败了即说明参数给多了,此时sizeof…(Args) - 1即为成员数量。

最后在用一个变量模板包装一下,这也符合标准库的习惯。

现在得到了成员数量,如何根据成员数量指定结构体绑定解包的数量,这里要借助宏来实现:

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

template <typename T, std::size_t n>
struct ReflectHelper{};

#define RFL_STRUCT(n, ...) \
template <class T> \
struct ReflectHelper<T, n> { \
static constexpr auto reflectFieldImpl() { \
auto& [__VA_ARGS__] = get_global_object<T>(); \
auto ref_tup = std::tie(__VA_ARGS__); \
return check_field(ref_tup); \
} \
}

RFL_STRUCT(1, f0);
RFL_STRUCT(2, f0, f1);
RFL_STRUCT(3, f0, f1, f2);
RFL_STRUCT(4, f0, f1, f2, f3);
RFL_STRUCT(5, f0, f1, f2, f3, f4);

template <typename T>
constexpr auto reflectField() {
return ReflectHelper<T, member_count_v<T>>::reflectFieldImpl();
}

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129

#include <iostream>

template <typename T>
struct Field {};

struct Dog
{
bool m_male;
};

struct Person
{
bool m_male;
Field<std::string> m_name;
Field<int> m_age;
};

class PersonPrivate {
Field<std::string> m_name;
Field<int> m_age;
bool m_male;
};

template<typename T>
constexpr auto g_value = T{};

template <auto val>
inline constexpr std::string_view getFunName() {
#if defined(__GNUC__)
constexpr std::string_view func_name = __PRETTY_FUNCTION__;
#elif defined(_MSC_VER)
constexpr std::string_view func_name = __FUNCSIG__;
#endif
size_t pos1 = func_name.find("val = ");
if (pos1 == std::string_view::npos) {
return {};
}

size_t pos2 = func_name.find(";", pos1 + 6);
if (pos2 == std::string_view::npos) {
return {};
}

return func_name.substr(pos1 + 6, pos2 - pos1 - 6);
}

struct AnyType {
template <typename T>
operator T();
};

template <typename T>
consteval size_t countMember(auto&&... Args) {
if constexpr (!requires { T{Args...}; }) {
return sizeof...(Args) - 1;
}
else {
return countMember<T>(Args..., AnyType{});
}
}

template <typename T>
constexpr size_t member_count_v = countMember<T>();

template <typename T>
struct Is_Field : std::false_type {
};

template <typename T>
struct Is_Field<Field<T>> : std::true_type {};

template <typename Tuple>
constexpr bool check_field(const Tuple& tuple) {
bool found = false;

std::apply([&](const auto&... args) {
((found = found || Is_Field<std::decay_t<decltype(args)>>()), ...);
}, tuple);

return found;
}

template <class T>
struct my_wrapper {
inline static T value;
};

template <class T>
inline constexpr T& get_global_object() noexcept {
return my_wrapper<T>::value;
}

template <typename T, std::size_t n>
struct ReflectHelper{};

#define RFL_STRUCT(n, ...) \
template <class T> \
struct ReflectHelper<T, n> { \
static constexpr auto getTuple() { \
auto& [__VA_ARGS__] = get_global_object<T>(); \
return std::tie(__VA_ARGS__); \
} \
static constexpr auto reflectFieldImpl() { \
auto ref_tup = getTuple(); \
return check_field(ref_tup); \
} \
}

RFL_STRUCT(1, f0);
RFL_STRUCT(2, f0, f1);
RFL_STRUCT(3, f0, f1, f2);
RFL_STRUCT(4, f0, f1, f2, f3);
RFL_STRUCT(5, f0, f1, f2, f3, f4);

template <typename T>
constexpr auto reflectField() {
return ReflectHelper<T, member_count_v<T>>::reflectFieldImpl();
}

int main() {
static_assert(reflectField<Person>);
constexpr auto tp = ReflectHelper<Person, member_count_v<Person>>::getTuple();
[&]<size_t... Is>(std::index_sequence<Is...>) {
(std::cout << ... << getFunName<&std::get<Is>(tp)>());
}(std::make_index_sequence<std::tuple_size_v<decltype(tp)>>{});
}