第一章:反射语法基础
对于模板了解不多的同学可以先学习下模板基础,这里推荐博客:模板元编程教程 ,讲的很详细。
以下是我们在实现反射时会用到的部分语法介绍
为什么选择 C++20
首先,实现一个反射功能就需要知道一个对象的:成员名称、成员的值,并且要有修改成员值的能力,而c++因为“零运行时开销”的设计初衷,导致反射迟迟没有被纳入标准(c++26已支持),因为反射往往意味着运行时元数据,这会带来额外内存和性能开销,与零开销原则冲突。
不过我们可以通过一些其他的方式,实现我们自己的简单的反射功能,不过这肯定会不可避免的带来:
代码体积膨胀(反射类型越多,体积越大)
运行时内存增加(运行时保留类型元数据)
不过在软件工程中,复杂度是守恒的,只会转移不会消失,所以一定程度的编码便利带来一定的开销也是可以接受的。
通过查找资料发现,目前c++20的反射实现主要思路都是:
- 通过结构化绑定和编译器内置的宏(
MSVC:__FUNCSIG__
、clang、gcc:__PRETTY_FUNCTION__
)来获得对象的成员名称 - 通过结构化绑定获得对象的成员引用(可以获得和修改对象数据)
这里的__FUNCSIG__
顾名思义是获得函数签名的宏,对于如下调用:
1
2
3
4
5
6
7
8
9
10
11
| template <auto val>
inline void get_func_name_template() {
std::cout << __FUNCSIG__ << std::endl;
}
struct Person {
int m_age;
std::string m_name;
};
static Person p;
get_func_name_template<&p.m_name>();
// MSVC C++20编译输出:void __cdecl get_func_name_template<&p.m_name>(void)
|
可以看到,输出中包含了我们需要的成员名称,对于这种固定模板,我们可以很容易的获得他的成员名称,但是如上代码在C++17是无法编译通过的,C++17需要如下格式:
1
| get_func_name_template<&p>();
|
原因在于:C++17对于非类型模板参数的值类型限制很严格,不允许将对象的子对象地址作为模板入参,这就直接导致了C++20以下的版本通常只能通过如下方式手动注册对象的元信息:
1
| REFLECTION(Person, m_age, m_name);
|
这就直接导致了想要一行代码实现序列化和反序列化,只能以C++20作为基础来实现。
而其他的限制通常都可以通过SFINAE
来规避,只不过实现起来会更加复杂。
同时C++20还有很多方便我们实现的特性,如:支持模板的Lambda表达式
,concept
和requires
等,能够大大提高我们的代码可读性,提高开发效率。
语法介绍
对模板Lambda表达式的支持
在 C++20 中,你可以让 Lambda 接收模板参数,这在编译期展开成员信息时非常方便。
1
2
3
4
5
| auto print_indices = []<std::size_t... Is>(std::index_sequence<Is...>) { // C++20支持模板
((std::cout << Is << " "), ...); // C++17 折叠表达式
};
print_indices(std::make_index_sequence<5>{});
// 输出:0 1 2 3 4
|
这里的折叠表达式能够将模板参数包进行展开,这种写法在后面会经常用到,是反射利器。
concept 与 requires
C++17 的 SFINAE(Substitution Failure Is Not An Error)
虽然可以限制模板的使用条件,但写法晦涩难懂,错误信息又长又吓人。C++20 的 concept
/ requires
让约束直接写在模板签名上,可读性高很多。
例子(判断类型是否可加):
1
2
3
4
5
6
7
8
9
10
11
12
| template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
template <Addable T>
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << add(3, 4) << "\n"; // OK
// std::cout << add("a", "b"); // 编译错误,error C2672: “add”: 未找到匹配的重载函数
}
|
对比 C++17:
1
2
3
4
| template <typename T, typename = std::enable_if_t<
std::is_convertible_v<decltype(std::declval<T>() + std::declval<T>()), T>
>>
T add(T a, T b) { return a + b; } // SFINAE
|
是不是瞬间清爽了?这就是 concept
/ requires
的魅力。
concept
相当于一个编译期的bool
值,可以直接使用在模板中做类型约束
requires
有以下几种写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 1
template <typename T> requires std::integral<T> // 直接写在模板尾部
T multiply_by_two(T x) {}
// 2
template <typename T>
T multiply_by_two(T x) requires std::integral<T> {} // 写在函数声明后
// 3 requires (参数列表) { 要检测的表达式; ... };
template <typename T>
concept Incrementable = requires(T x) { // 对模板参数做多重约束
{ ++x } -> std::same_as<T&>; // 支持使用++运算符并且返回类型必须是 T&
{ --x }; // 支持--运算符
};
|
在c++20中,约束也被算作重载决议的一种,所以对于如下代码:
1
2
3
4
5
6
7
8
| template <OutputStream Stream, typename T>
inline void to_json_value(Stream&& s, T&& object) requires is_custom_type_v<T>;
template <OutputStream Stream, typename T>
inline void to_json_value(Stream&& s, T&& object) requires is_sequence_container_v<T>;
template <OutputStream Stream, typename T>
inline void to_json_value(Stream&& s, T&& object) requires is_associative_container_v<T>;
|
通过使用requires
能够非常清爽的实现对不同类型的不同处理,可读性很高,如果按照SFINAE
写法如下:
1
2
| template <class S, class T, std::enable_if_t<is_custom_type_v<T>, int> = 0>
void to_json_value(S&&, T&&);
|
其中对于类型S和T想要进一步约束还会更复杂。
结构化绑定(Structured Bindings)和tuple
结构化绑定是 C++17 引入的语法,让你可以用类似解构赋值的方式,直接把一个struct
或 tuple
拆成多个变量。
1
2
3
4
5
| std::tuple<int, double, char> t{42, 3.14, 'x'}; // tuple
auto [i, d, c] = t;
struct Person {int m_age; std::string m_name; }; // struct
auto [a, n] = p; // 按值绑定
auto& [ra, rn] = p; // 按引用绑定
|
在编译期反射中,结构化绑定是获取成员引用 tuple的核心手段之一。
std::tuple
是 C++ 标准库里的一种固定长度、多类型的集合容器,从定义看就非常适合作为结构体元信息的存储单位。
tuple
构造方式如下:
1
| std::tuple<int, double, std::string> t1(42, 3.14, "hello");
|
也可以通过std::make_tuple
进行构造:
1
| auto t = std::make_tuple(42, 3.14, std::string("hello")); // 类型自动推导:std::tuple<int, double, std::string>
|
通过make_tuple
构造tuple
会通过值拷贝的方式进行构造
还可以通过std::tie
进行构造:
1
| auto t = std::tie(person.m_age, person.m_name);
|
这里构造出的tuple
会持有原对象的引用,可以通过std::get
拿到引用并对值做修改
可变参数模板与参数包展开(Variadic Templates & Pack Expansion)
这是 C++11 引入的重要语法,用来处理任意数量的模板参数
可变参数模板
1
2
3
4
5
6
7
8
| template <typename... Args>
void print_all(Args... args) {
(std::cout << ... << args) << "\n"; // C++17 折叠表达式
}
int main() {
print_all(1, 2.5, "hello"); // 输出:12.5hello
}
|
参数包展开
参数包展开就是用 ...
把一段模式按参数包逐项替换,然后拼成一串。
1
2
3
4
| template <typename... Ts>
using MyTuple = std::tuple<Ts...>;
using T = MyTuple<int, double, std::string>; // 等价于 tuple<int, double, string>
|
例子(反射中常见模式展开):
1
2
3
4
5
6
7
| template <std::size_t... Is>
constexpr std::array<int, sizeof...(Is)> get_array(std::index_sequence<Is...>) {
return std::array<int, sizeof...(Is)>{ { static_cast<int>(Is)... } };
}
get_array(std::make_index_sequence<5>{});
// 构造array: [1, 2, 3, 4, 5]
|
这里的 ...
就是参数包展开,它会依次替换 Is
,展开为:{1, 2, 3, 4, 5}
。
本章总结
- C++20 的三大升级(
模板 Lambda
、concept/requires
、NTTP 放宽
)是非侵入式反射的关键。 - 结构化绑定让我们在编译期轻松获取成员引用。
- concept 与 requires让模板约束变得优雅可读。
- 可变参数模板与参数包展开让我们能在编译期遍历所有成员类型,并生成序列化/反序列化代码。
下一章,我们会在这些语法基础上,开始构建编译期静态反射的核心机制,看看如何一步步获取结构体的成员数量、成员引用 tuple、成员名称。