第六章:编译期反射 private 成员

上一章我们已经介绍了对于聚合类型的编译期反射,本章我们介绍对于非聚合类型如何反射。

首先看一下知乎的 YKIKO 在有状态黑魔法中提到的例子:
👉 https://zhuanlan.zhihu.com/p/646752343

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

class Bank {
double money = 999'999'999'999;

public:
void check() const { std::cout << money << std::endl; }
};

template <auto mp>
struct Thief {
};

template struct Thief<&Bank::money>;

这里的 &Bank::money 是一个成员变量指针,不同于普通的指针,它并不依赖于某个对象,而表示该成员在类中的偏移量,是一个编译期常量

在 C++ 中,成员指针(Pointer-to-Member)普通指针(Pointer-to-Object/Pointer-to-Function) 是两种不同的概念,它们的类型、语法和用途都有显著区别。下面详细对比它们的差异:


1. 类型定义

(1)普通指针

  • 指向对象T*(指向类型 T 的对象)
  • 指向函数R (*)(Args...)(指向返回 R、参数为 Args... 的函数)
  • 示例
    1
    2
    3
    4
    5
    int x = 10;
    int* ptr = &x; // 普通指针,指向 int 对象

    void foo(int);
    void (*func_ptr)(int) = &foo; // 普通函数指针

(2)成员指针

  • 指向成员变量T C::*(指向类 C 的成员变量,类型为 T
  • 指向成员函数R (C::*)(Args...)(指向类 C 的成员函数)
  • 示例
    1
    2
    3
    4
    5
    6
    7
    struct Bank {
    double money;
    void check() const;
    };

    double Bank::* money_ptr = &Bank::money; // 成员变量指针
    void (Bank::* check_ptr)() const = &Bank::check; // 成员函数指针

2. 使用方式

(1)普通指针

  • 直接解引用访问:
    1
    2
    3
    int x = 10;
    int* ptr = &x;
    *ptr = 20; // 直接修改 x

(2)成员指针

  • 必须结合对象 才能访问:
    1
    2
    3
    4
    5
    6
    Bank bank;
    double Bank::* money_ptr = &Bank::money;
    bank.*money_ptr = 100; // 等价于 bank.money = 100

    void (Bank::* check_ptr)() const = &Bank::check;
    (bank.*check_ptr)(); // 等价于 bank.check()

3. 存储方式

特性 普通指针 成员指针
存储地址 直接存储对象/函数的地址 存储的是 相对于类对象的偏移量(成员变量)或 函数地址+调整信息(成员函数)
是否依赖对象 可以直接解引用 必须绑定到对象才能使用(obj.*ptrobj->*ptr
sizeof 大小 通常等于机器字长(如 8 字节) 可能比普通指针大(成员函数指针可能占用 2 个机器字)

👀 显示实例化与私有访问权限

这里还用到了模板显示实例化时会忽略类作用域访问权限的特性。

🔍 什么是显示实例化?

1
2
template class MyClass<int>;        // 显式实例化类模板
template void myFunc<double>(); // 显式实例化函数模板

这强制编译器在该位置生成实例代码,而不是等到首次使用。

✅ 优点

  • 减少编译时间
  • 减少重复实例化
1
2
// MyClass.cpp
template class MyClass<int>; // 实例化

其他文件:

1
extern template class MyClass<int>; // 声明:避免重复生成

⚠️ 无法访问 private 成员的问题

但是问题来了,即使我们显示实例化了Thief<&Bank::money>,编译器也帮我们生成了这样的代码:

1
2
3
4
5

template <&Bank::money>
struct Thief {
};

你甚至已经想好了利用Thief的类型参数做各种操作:

1
2
3
4
5
6

template <mp = &Bank::money>
struct Thief {
double& steal(Bank& bank) { return bank.*mp; }
static double& steal2(Bank& bank) { return bank.*mp; }
};

但是有什么用呢,你无法使用Thief中的任何成员函数,因为Thief本身是一个类模板,想使用Thief必须要指定类型参数。
一旦你指定了Thief<&Bank::money>去隐式实例化一个Thief,会立马报错因为无法访问&Bank::money。

那么我显示实例化生成的代码岂不是卵用没有?

这就需要结合友元函数来使用,友元函数与普通函数最大的不同就在于不要求函数定义与函数声明在同一scope中,所以我们就可以非Thief的作用域下使用它。

具体来说就是在全局的scope去声明该友元函数;而在Thief<&Bank::money>的scope去定义友元函数,同时窃取它的非类型参数,这样我们就可以在全局的scope去使用它,绕过了必须隐式实例化Thief才能使用它的限制。

🪄 解决方案:友元函数 + 显示实例化

利用友元函数的特性,可以在类模板内部定义,在外部声明,从而绕开访问权限。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

class Bank {
double money = 999'999'999'999;

public:
void check() const { std::cout << money << std::endl; }
};

template <auto mp>
struct Thief {
friend double& steal(Bank& bank) { return bank.*mp; }
};

double& steal(Bank& bank); // #1
template struct Thief<&Bank::money>; // #2

int main() {
Bank bank;
steal(bank) = 100; // #3
bank.check(); // 输出 100
}

我曾经幻想利用这个机制实现一个thief的库,把显式实例化和友元函数都封装在库中,用户只需注册成员就可以直接使用库中的steal方法获取私有成员。

1
2
3
4
5
6
7
8

int main() {
Bank bank;
REGISTER(Bank, money);
steal(bank) = 100;
bank.check();
}

但是显式实例化必须在全局命名空间 或 命名空间作用域,不能出现在函数内部:

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

#include <iostream>

template <auto mp>
struct Thief {
template <typename T>
friend double& steal(T&& t) {
return t.*mp;
}
};
template <typename T>
double& steal(T&& t);

#define REGISTER(STRUCT, MEMBER) \
template struct Thief<&STRUCT::MEMBER>;

class Bank {
double money = 999'999'999'999;

public:
void check() const { std::cout << money << std::endl; }
};


int main() {
Bank bank;
REGISTER(Bank, money); // error
steal(bank) = 100;
bank.check();
}

这是因为显式实例化声明和定义(如 template struct Thief<&Bank::money>;)在 C++ 标准中被限定只能出现在命名空间作用域(即:全局作用域或某个命名空间中),不能放在函数体(比如 main)里。这是出于以下几个原因:


✅ 1. 语言标准限制(语法层面)

C++ 标准([C++20 §14.7.2])明确指出:

An explicit instantiation shall appear in a namespace scope (not inside a function or class).

这是语法层面的硬性规定,编译器会直接报错,不予接受。


✅ 2. 链接模型设计:实例化必须唯一可见

显式实例化通常意味着「我手动生成这个模板的具体版本,别再自己自动生成了」,这影响符号的生成和链接行为

  • 如果你允许它在函数内,会在函数作用域内临时生成一个实例;
  • 但别的地方可能还会再实例化一次,违反 One Definition Rule(ODR);
  • 放在全局作用域可以控制符号的唯一性,符合链接器模型。

🧩 泛化实现:反射 private 成员

1
2
3
4
5
6
7
8
9
10
11
template <typename T, auto... field>
struct private_visitor {
friend inline constexpr auto get_private_ptrs() {
constexpr auto tp = std::make_tuple(field...);
return tp;
}
};

#define REFL_PRIVATE(STRUCT, ...) \
inline constexpr auto get_private_ptrs(); \
template struct private_visitor<STRUCT, ##__VA_ARGS__>;

示例:

1
2
3
4
5
6
class PersonPrivate {
Field<std::string> m_name;
Field<int> m_age;
bool m_male;
};
REFL_PRIVATE(PersonPrivate, &PersonPrivate::m_name, &PersonPrivate::m_age, &PersonPrivate::m_male)

🧭 为了避免 ODR 冲突 —— 添加类型参数

1
2
3
4
5
6
7
8
9
10
11
template <typename T, auto... field>
struct private_visitor {
friend inline constexpr auto get_private_ptrs(const my_wrapper<T>&) {
constexpr auto tp = std::make_tuple(field...);
return tp;
}
};

#define REFL_PRIVATE(STRUCT, ...) \
inline constexpr auto get_private_ptrs(const my_wrapper<STRUCT> &t); \
template struct private_visitor<STRUCT, ##__VA_ARGS__>;

🧠 SFINAE 萃取成员类型

我们可以从成员指针中推导其类型:

1
2
3
4
5
6
7
8
9
10
11
template <auto MemberPtr>
struct MemberPointerTraits;

template <typename T, typename C, T C::*MemberPtr>
struct MemberPointerTraits<MemberPtr> {
using type = T;
using class_type = C;
};

template <auto MemberPtr>
using member_value_v = typename MemberPointerTraits<MemberPtr>::type;

至此,我们实现了:

  • ✅ 聚合类型的反射
  • ✅ 非聚合类型的反射
  • ✅ 编译期萃取字段类型
  • ✅ 编译期获取字段名称

真正意义上的非侵入式编译期反射机制

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172

#include <iostream>

template <typename T>
struct Field {};

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

template <typename T, auto... field>
struct private_visitor {
friend inline constexpr auto get_private_ptrs(const my_wrapper<T>&) {
constexpr auto tp = std::make_tuple(field...);
return tp;
}
};

#define REFL_PRIVATE(STRUCT, ...) \
inline constexpr auto get_private_ptrs(const my_wrapper<STRUCT> &t); \
template struct private_visitor<STRUCT, ##__VA_ARGS__>;

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;
};
REFL_PRIVATE(PersonPrivate, &PersonPrivate::m_name, &PersonPrivate::m_age, &PersonPrivate::m_male)

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 <class T>
inline constexpr T& get_global_object() noexcept {
return my_wrapper<T>::value;
}

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 <auto ptr>
void f() {
#if defined(__GNUC__)
constexpr std::string_view func_name = __PRETTY_FUNCTION__;
#elif defined(_MSC_VER)
constexpr std::string_view func_name = __FUNCSIG__;
#endif
std::cout << func_name << std::endl;
}

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 <auto ptr>
inline constexpr std::string_view get_member_name() {
#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;
}

template <auto MemberPtr>
struct MemberPointerTraits;

template <typename T, typename C, T C::*MemberPtr>
struct MemberPointerTraits<MemberPtr> {
using type = T;
using class_type = C;
};

template <auto MemberPtr>
using member_value_v = typename MemberPointerTraits<MemberPtr>::type;

template <typename T>
concept IsAggregate = std::is_aggregate_v<T>;

template <typename T>
struct ReflectField {
static constexpr bool reflect() {
constexpr auto tp = get_private_ptrs(my_wrapper<T>{});
constexpr size_t N = std::tuple_size_v<decltype(tp)>;
bool found = false;

[&]<size_t... Is>(std::index_sequence<Is...>) {
((found = found || Is_Field<member_value_v<std::get<Is>(tp)>>()), ...);
}(std::make_index_sequence<N>{});

return found;
}
};

template <IsAggregate T>
struct ReflectField<T> {
static constexpr auto reflect() {
return ReflectHelper<T, member_count_v<T>>::reflectFieldImpl();
}
};

template <typename T>
constexpr bool reflectField_v = ReflectField<T>::reflect();

int main() {
static_assert(!reflectField_v<Dog>);
static_assert(reflectField_v<Person>);
static_assert(reflectField_v<PersonPrivate>);
constexpr auto tp = get_private_ptrs(my_wrapper<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)>>{});
}