Loading [MathJax]/extensions/tex2jax.js
RMVL  
Robotic Manipulation and Vision Library
全部  命名空间 文件 函数 变量 类型定义 枚举 枚举值 宏定义  
聚合类反射及其相关 API

聚合体的编译期反射,包含 C++17 和 C++20 两个版本的实现原理

作者
赵曦
日期
2023/09/19
版本
1.0

下一篇教程:串口通信模块


1. 聚合类

有关聚合体的概念可直接参考 cppreference 手册,这里对聚合体的其中一个用法做介绍。

数组类型同样属于聚合体,这里仅探究符合聚合体定义的类类型,我们称其为聚合类,聚合类允许聚合初始化,即从初始化列表初始化聚合类,例如

struct Person
{
int age;
double height;
std::string name;
};
void init()
{
Person p3{18, 1.78, "zhaoxi"};
}

cppreference 给出聚合初始化的一个效果是

按以下规则决定显式初始化元素:

  • 性质 1 : 如果初始化列表是指派初始化列表(聚合体此时只能具有类类型),每个指派符中的标识符必须指名该类的一个非静态数据成员,并且聚合体中显式初始化元素是这些成员或包含这些成员的元素。 (C++20 起)
  • 性质 2 : 否则,如果初始化列表不为空,那么显式初始化元素是聚合体中的前 n 个元素,其中 n 是初始化列表中的元素数量。
  • 性质 3 : 否则初始化列表必须为空,并且不存在显式初始化元素。

根据 性质 2 ,聚合初始化的代码写成如下形式

void init()
{
Person p1{18}; // OK
Person p2{18, 1.78}; // OK
Person p3{18, 1.78, "zhaoxi"}; // OK
}

这三种都是正确的,但是初始化列表中的元素个数不能超过聚合体的元素个数。

void init()
{
Person p4{18, 1.78, "zhaoxi", 'a'}; // 非良构
}

根据这一特点我们可以实现

  • 编译期间得知任意聚合体 T(这里给出的是聚合体,说明数组也成立)的元素个数
  • 遍历聚合体的所有元素

的编译期反射机制。

2. 获取聚合体元素个数

下文给出 C++17 和 C++20 两个版本的实现方法,点击对应按钮可查看详细内容

参见
core/util.hpp

3. 其余聚合体反射工具

3.1 成员遍历

函数原型如下

template <typename Tp, typename Callable>
void for_each(const Tp &val, Callable &&f);

其中

  • Tp — 聚合类型(必须满足)
  • Callable — 可调用对象类型,可以是函数模板,函数对象模板,或者 lambda 表达式的简写模板(即 auto && 类型)

下面给出一个例子

struct X
{
int a{};
double bb{};
std::string str{};
};
int main()
{
auto f = [](auto &&val) {
std::cout << "val = " << val << std::endl;
};
X x{1, 3.1, "abc"};
rm::for_each(x, f);
};
@ X
X 轴
定义 transform.hpp:29

编译后运行结果为

val = 1
val = 3.1
val = abc

实现方法简单粗暴,通过结构化绑定与编译期的 constexpr-if 语句,分别调用可调用对象 f 即可,例如,一个 size = 3 的聚合体,可以使用以下语句完成成员的遍历

const auto &[m0, m1, m2] = val;
f(m0), f(m1), f(m2);

3.2 相等函数

注解
  • 此操作主要用于 C++20 前,自定义类的自定义 hash 函数
  • C++20 起,可使用预置 operator== 函数来实现同样的功能,可参考默认比较

函数原型如下

template <typename Tp>
bool equal(const Tp &lhs, const Tp &rhs);

其中

  • Tp — 聚合类型(必须满足)

下面给出一个例子

struct X
{
int a{};
double bb{};
std::string str{};
};
int main()
{
X x1{1, 3.1, "abc"};
X x2{2, 4.1, "abc"};
X x3{1, 3.1, "abc"};
std::cout << "x1 = x2: " << std::boolalpha << rm::reflect::equal(x1, x2) << std::endl;
std::cout << "x1 = x3: " << std::boolalpha << rm::reflect::equal(x1, x3) << std::endl;
};
bool equal(const Tp &lhs, const Tp &rhs)
判断两个聚合类数据是否相同
定义 util.hpp:414

编译后运行结果为

x1 = x2: false
x1 = x3: true

实现方法同样简单粗暴,核心操作与 for_each 的几乎一致,代码如下

const auto &[l0, l1, l2] = lhs;
const auto &[r0, r1, r2] = rhs;
return l0 == r0 && l1 == r1 && l2 == r2;

4. 聚合类对象作为散列表的键 (Key)

cppreference 中给出了有关自定义散列函数的例子

struct S
{
std::string first_name;
std::string last_name;
bool operator==(const S&) const = default; // C++20 起
};
// C++20 前
// bool operator==(const S& lhs, const S& rhs)
// {
// return lhs.first_name == rhs.first_name && lhs.last_name == rhs.last_name;
// }
// 自定义散列函数可以是独立函数对象:
struct MyHash
{
std::size_t operator()(S const& s) const
{
std::size_t h1 = std::hash<std::string>{}(s.first_name);
std::size_t h2 = std::hash<std::string>{}(s.last_name);
return h1 ^ (h2 << 1); // 或者使用 boost::hash_combine
}
};

RMVL 提供了聚合类一般化的接口,即任意聚合类的自定义散列函数 rm::hash_aggregate,基本用法如下

struct X
{
int a{};
double bb{};
std::string str{};
#if __cplusplus < 202002L
bool operator==(const X &s) const { return rm::reflect::equal(*this, s); }
#else
bool operator==(const X &) const = default;
#endif
};
void f()
{
// 定义 Key = X,Val = int 的散列表
std::unordered_map<X, int, rm::hash_aggregate<X>> hashmap;
}

此外,对于一般化的类型 T,如果要使用 std::unordered_map,可以使用 类型特性 type traits 相关功能,RMVL 提供了用于 hash 函数选择的类型特性: rm::hash_traits,类型名为 hash_func,下面给出一个简单的用法

template <typename T>
void f()
{
std::unordered_map<T, int, rm::hash_traits<T>::hash_func> hashmap;
}