C++26 编译时反射简介
什么是反射?
反射是指程序观察自身的结构, 并且可以获取到有关它的信息. 比如获取结构体的字段(Field/Member)及其类型, 获取方法(Method), 检查是否存在特定的方法.
反射可以用来做代码生成, 它可以大大减少样板代码. 使用场景有: 结构体的序列化和反序列化, 可以避免繁琐的手写代码. 在日常业务中, C++ 解析json
或者xml
时, 需要手写序列化/反序列化的代码, 这个过程需要很多的重复代码, 且没有多少技术含量.
当前的提议
当前的反射功能的提议为: P2996 “Reflection for C++26”, Daveed Vandervourde, Wyatt Childers, Peter Dimov, Dan Katz, Barry Rezvin, Andrew Sutton, Faisil Vali.
这个提案基于为 P1240 所做的工作, 是一个最小可行提案, 是一个编译时且基于函数的 API, 所有函数都是 consteval
.
主要有三部分组成:
- 反射信息
std::meta::info
. 是不透明标量类型, 保存的是反射得到的信息. 一切反射结果都使用一种类型, 仅在编译时有用. - 反射操作符
^
. 用来执行反射操作. - Splicer
[: :]
. 将反射值转换为代码. 用来将反射操作符得到的值应用到代码中.
反射操作符 ^
反射操作符^
用来将语法构造转换为反射值, 它是前缀一元操作符, 它的返回值是一个 std::meta::info
对象.
可以用在如下场景:
-
类型
constexpr std::meta::info r1 = ^int; constexpr std::meta::info r2 = ^std::vector<int>; constexpr std::meta::info r3 = ^std::string;
-
命名空间, 包括全局命名空间
::
constexpr std::meta::info r4 = ^::; constexpr std::meta::info r5 = ^std::chrono;
-
常量表达式
constexpr std::meta::info r6 = ^(std::barrier<>::max() - 100);
-
一个符号名: 函数, 变量, 结构化绑定, 模板, 概念
constexpr std::meta::info r7 = ^std::vector; constexpr std::meta::info r8 = ^std::fopen;
Splicer [: :]
用来将反射值转换为代码, 它的操作数是一个 std::meta::info
对象, 有时候也可以存在前置的typename
或者template
.
-
内置类型:
#include <experimental/meta> #include <iostream> #include <string> #include <type_traits> #include <vector> int main() { //{ constexpr auto r = ^int; typename[:r:] x = 42; // 等价于: int x = 42; std::vector<typename[:r:]> v; // 等价于: std::vector<int> v; typename[:^double:] d = 3.14; // 等价于: double d = 3.14; std::cout << "x=" << x << ", d=" << d << std::endl; //} }
EDG 和 NVC++ 24.3 目前能编译通过, 点击链接在 Compiler Explorer 中查看.
-
函数类型
#include <experimental/meta> #include <iostream> int f(int a, int b) { return a + b; } constexpr auto func = ^f; int main() { int (*fp)(int, int) = &[:func:]; std::cout << [:func:](1, 2) + fp(3, 4); }
在 Compiler Explorer 中查看: NVC++ 24.3
-
结构体成员
#include <experimental/meta> #include <iostream> struct S { int field; }; int main() { constexpr auto member = ^S::field; S a; a.[:member:] = 42; std::cout << "a." << std::meta::name_of(member) << "=" << a.[:member:]; }
在 Compiler Explorer 中查看: NVC++ 24.3
元函数
元函数的输入为std::meta::info
, 用来获取反射值相关的信息. 目前有如下的元函数:
consteval bool is_namespace(info r);
consteval bool is_function(info r);
consteval bool is_variable(info r);
consteval bool is_type(info r);
consteval bool is_alias(info r);
consteval bool is_template(info r);
consteval bool is_concept(info r);
consteval bool is_class_member(info r);
consteval bool is_base(info r);
consteval string_view name_of(info r);
consteval string_view qualified_name_of(info r);
consteval string_view display_name_of(info r);
consteval source_location source_location_of(info r);
下面的代码展示了调用一个结构体的size()
或者 length()
方法.
template <typename T>
int size_or_length(T&& x) {
if constexpr (std::is_class_v<T>) {
template for (constexpr auto memfunc :
members_of(^T, std::meta::is_function)) {
if constexpr ((name_of(memfunc) == "size" ||
name_of(memfunc) == "length") &&
requires(T y) {
{ y.[:memfunc:]() } -> std::integral;
}) {
return x.[:memfunc:]();
}
};
}
return -1;
}
使用样例
1. 枚举转字符串
#include <experimental/meta>
#include <string>
#include <type_traits>
template<typename E>
requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
std::string result = "<unnamed>";
[:expand(std::meta::enumerators_of(^E)):] >>
[&]<auto e>{
if (value == [:e:]) {
result = std::meta::identifier_of(e);
}
};
return result;
}
enum Color { red, green, blue };
static_assert(enum_to_string(Color::red) == "red");
static_assert(enum_to_string(Color(42)) == "<unnamed>");
在 Compiler Explorer 中在线运行.
2. 解析命令行参数
#include <string>
#include <experimental/meta>
#include <iostream>
#include <algorithm>
#include <spanstream>
#include <type_traits>
template<typename Opts>
Opts parse_options(int argc, char** argv) {
std::vector<std::string_view> args(argv + 1, argv + argc);
Opts opts;
[: expand(nonstatic_data_members_of(^Opts)) :] >> [&]<auto dm>{
auto it = std::find_if(args.begin(), args.end(),
[](std::string_view arg){
return arg.starts_with("--") && arg.substr(2) == identifier_of(dm);
});
if (it == args.end()) {
return;
}
using T = typename[:type_of(dm):];
if constexpr (std::is_same_v<T, bool>) {
opts.[:dm:] = true;
} else if (it + 1 == args.end()) {
std::cerr << "Missing value for option " << *it << "\n";
} else {
auto iss = std::ispanstream(it[1]);
if (iss >> opts.[:dm:]; !iss) {
std::cerr << "Invalid value \"" << it[1] << "\" for option " << *it << "\n";
}
}
};
return opts;
}
struct app_options {
int iterations = 1'000;
int size = 25'000;
bool verbose = false;
std::string inputFile = "input.dat";
};
int main(int argc, char *argv[]) {
app_options opts = parse_options<app_options>(argc, argv);
std::cout << "iterations: " << opts.iterations << "\n"
<< "size: " << opts.size << "\n"
<< "verbose: " << opts.verbose << "\n"
<< "inputFile: " << opts.inputFile << "\n";
}
在 Compiler Explorer 中在线运行.
总结
- C++ 反射功能即将到来
- C++ 反射是编译时的功能
- C++反射能够减少手工编写样板代码, 减少重复代码.
参考资料
- C++ Reflection - Back on Track by David Olsen
- Reflection for C++26
延伸阅读
- C++26 新特性预览