Part 0. 前言
本文受alibaba yalantinglibs启发,旨在学习利用C++17加入的结构化绑定特性实现一些编译期的反射技术,同时采用与yalantinglibs相同或类似的实现方式,以便读者自行探索yalantinglibs中的其他部分。
Part 1. 聚合类型(Aggregate Class)的编译期反射方法
1.1 将聚合类型对象反射为tuple
特性1:结构化绑定:
对于一个聚合类型的结构体,可以绑定变量到这个结构体的数据成员
struct Person
{
char name[8];
int age;
double salary;
};
int main()
{
auto [n, a, s] = Person{"Allen", 42, 1000.0};
// char[8] n, int a, double s
}
借助这一特性,可以在编译期获取结构体数据成员的类型:
using type_of_name = decltype(n);
对于std::tuple<T...>
类型,结构化绑定同样适用:
auto [x, y, z] = std::tuple<int, double, char>{1, 2, '3'};
组合上面两点,我们便可以用函数来传递“分解”结构体的结果:
template<typename T>
constexpr auto tuple_view_of_members(T& t)
{
auto& [x, y, z] = t;
return std::tie(x, y, z); // 这个函数存在明显不足,详见下文1.2, 1.3节
}
1.2 计算聚合类型对象的成员个数
对于聚合类型的初始化,要么提供其全部成员的初始值,要么提供部分成员的初始值,总之不能超过其成员个数——这给了我们一个计算聚合类型对象成员个数的方法:通过不断添加聚合类型初始化器中的参数个数,来试探出该类型的成员个数:
struct A {
// some members
};
A { T{} }; // 可以构造
A { T{}, T{} }; // 可以构造
A { T{}, T{}, T{} }; // 无法构造,说明A有两个成员
要初始化一个聚合类型中的数据成员,我们应该提供和对应数据成员相同类型的值或者能够转换成数据成员类型的类型的值:
struct A {
int a;
double c;
};
A { 1, 2.0 }; // ok
A { T{}, 2.0 }; // ok if `T` have operator int()
由于在编写程序时我们无法预测某一聚合类型中究竟有什么类型的成员,所以我们选择使用一个“通用”类型来代替使用具体类型,即让这一类型可以隐式转换成其他类型:
struct UniversalType
{
template<typename T>
operator T()
{ } // 在不求值语境,例如decltype内可以删去定义,只留下声明
};
那么下面的表达式就合法了:
struct A {
int a;
double b;
} a{ UniversalType{}, UniversalType{} };
现在,我们需要利用的是通过这一通用类型去构造目标类型产生表达式的合法性来计算聚合类型的成员个数:
template<typename T, typename construct_param_t, typename = void typename ...Args>
struct is_constructible_impl : std::false_type {};
template<typename T, typename construct_param_t, typename ...Args>
struct is_constructible_impl<
T, construct_param_t,
std::void_t<decltype(T{ {Args{}}..., {construct_param_t{}} }),
Args...>
: std::true_type {};
template<typename T, typename construct_param_t, typename ...Args>
constexpr auto is_constructible_v = is_constructible_impl<T, construct_param_t, void, Args...>::value;
这里的核心思想就是:如果decltype
内表达式合法,即类型T
能被按照指定的参数构造,那么is_constructible_impl<T, construct_param_t, void, Args...>
就会被匹配到继承true_type
的那一个特化,从而使表达式值为true
,反之则使表达式值为false
。(参考SFINAE)
在上面的实现中,我们将用多少个参数去构造类型T
取决于construct_param_t
和Args...
。利用sizeof...
和模板,我们可以设计一个递归函数不断地向is_constructible_v
中添加参数,直到提供参数的数量超过类型T
的成员数量,即T
无法被构造时停止,来获取类型T
的成员个数:
template<typename T, typename ...Args>
constexpr size_t members_count_impl()
{
if (is_constructible_v<T, UniversalType, Args...>) {
// 这里需要注意顺序,传入的UniversalType对应construct_param_t,是传入
// 类型T构造器的最后一个参数,但是按照一般思维,先传入的参数总是对应构造器
// 前面的参数或者前面的数据成员,因此这里需要把Args...放在前面,新的
// UniversalType放在后面
return members_count_impl<T, Args..., UniversalType>();
} else {
return sizeof...(Args);
}
}
在上面的函数中,会依次尝试构造T{UniversalType{}}
、T{UniversalType{}, UniversalType{}}
……,同时在函数模板的Args...
中记录UniversalType
的总数,
达到终止条件后,用sizeof...(Args)
即可求出类型T
可被多少个UniversalType
构造,即类型T
的成员个数。
当然,上面的程序仍然存在缺陷,例如某些特殊的类型可能无法简单地通过UniversalType
转换得到,又或者某些类型需要单独标出,为其他目的或未来扩展使用,这时就要额外添加检查分支,来保证程序正常运作:
if constexpr (is_constructible_v<T, UniversalType, Args...>) {
return members_count_impl<T, Args..., UniversalType>();
} else if (is_constructible_v<T, SpecialType, Args...>) {
return members_count_impl<T, Args..., SpecialType>();
} else {
return sizeof...(Args);
}
这里给出一个UniversalType
无法适配的例子:
struct NotUniversal {
NotUniversal() = default;
explicit NotUniversal(const NotUniversal&);
NotUniversal(std::vector<char>);
};
struct VectorType {
operator std::vector<char>();
};
static_assert(is_constructible_v<NotUniversal, UniversalType>, ""); // error
static_assert(is_constructible_v<NotUniversal, VectorType>, ""); // alright
谈到这里,我们已经有方法获取聚合类型的成员数量,接下来就可以对1.1节的内容进行改进了
1.3 将聚合对象反射为tuple方法的改进
在1.1节的程序中,我们尝试结构化绑定时需要知道要用多少个变量来接受结构化绑定的结果,即需要知道目标类型对象的成员个数。在C++26中,这一问题可以用结构化绑定包来解决:
auto& [x, ...r] = t;
return std::tie(x, r...);
但在C++26之前,我们就要另寻他法来解决这一问题。
我们先让某一类型来固定处理拥有某一数量成员的结构体:
struct visitor_1 {
template<typename T>
static constexpr auto tuple_view_of_members(T& t)
{
auto& [x] = t;
return std::tie(x);
}
};
struct visitor_2 {
template<typename T>
static constexpr auto tuple_view_of_members(T& t)
{
auto& [x, y] = t;
return std::tie(x, y);
}
};
但这样在使用处又存在问题:我们需要写很多的if分支,来确定具体调用哪一个visitor
类型。
这时可以考虑确定visitor类型的过程交给编译器,即让编译器选取特定的visitor特化:
template<std::size_t n>
struct visitor;
template<std::size_t n>
struct visitor<1> {
template<typename T>
static constexpr auto tuple_view_of_members(T& t)
{
auto& [x] = t;
return std::tie(x);
}
};
于是使用处只需这样调用即可:
visitor<members_count_impl<T>()>::tuple_view_of_members<T>();
如果觉得写两个尖括号太麻烦,可以将template<typename T>
提到visior的定义上。
这样,我们只用写visitor的不同特化即可处理拥有不同成员个数的聚合类型,而且由于特化的格式是固定的,我们甚至可以利用脚本来生成不同数量的特化。
这里可以采用宏来固定格式,只用改变传入宏的参数即可生成对应特化,简化了脚本编写的逻辑:
#define GENERATOR(n, ...) \
template<typename T> \
struct visitor<T, n> { \
template<typename T> \
static constexpr auto tuple_view_of_members(T& t) \
{ \
auto& [__VA_ARGS__] = t; \
return std::tie(__VA_ARGS__); \
} \
}
GENERATOR(1, f1);
GENERATOR(2, f1, f2);
GENERATOR(3, f1, f2, f3);
到这里,我们就较好地实现了编译期访问聚合类型对象的方法。