RMVL
2.0.0
Robotic Manipulation and Vision Library
|
聚合体的编译期反射,包含 C++17 和 C++20 两个版本的实现原理
下一篇教程:串口通信模块 ↓
有关聚合体的概念可直接参考 cppreference 手册,这里对聚合体的其中一个用法做介绍。
数组类型同样属于聚合体,这里仅探究符合聚合体定义的类类型,我们称其为聚合类,聚合类允许聚合初始化,即从初始化列表初始化聚合类,例如
cppreference 给出聚合初始化的一个效果是
按以下规则决定显式初始化元素:
(C++20 起)
根据 性质 2 ,聚合初始化的代码写成如下形式
这三种都是正确的,但是初始化列表中的元素个数不能超过聚合体的元素个数。
根据这一特点我们可以实现
T
(这里给出的是聚合体,说明数组也成立)的元素个数的编译期反射机制。
下文给出 C++17 和 C++20 两个版本的实现方法,点击对应按钮可查看详细内容
由 性质 2 可以知道,对于元素个数为 \(n\) 的任意聚合体类型 T
,在 T
类实例化对象的时候,初始化列表元素个数 \(m\) 需要满足 \(m\le n\) ,如果 \(m>n\) 则程序非良构。
我们可以有这样一个基本想法,可以给定一个比较大的 \(m\) 作为初值进行构造尝试,如果构造不成功则继续构造 \(m-1\) 的,直到成功构造为止,此时就满足 \(m=n\),因此返回此时的 \(m\) 值即可作为当前聚合体的元素个数 \(n\)。
由于 C++17 缺乏概念机制,因此我们需要通过人为制造具有若干重载版本的函数,并利用模板函数返回类型、形参类型的替换失败来实现该功能,一个最经典的做法是 SFINAE。可以通过函数模板形参在发生替换时非良构,从而删除该函数的其中之一个特化版本。
有了以上语言特性的支撑,我们回到问题最开始的地方 给定一个比较大的 m 作为初值进行构造尝试 。对于任意聚合体类型 T
,其元素类型也是任意的,我们通过一个包含不求值表达式 decltype
的后置返回类型的模板函数,来实现大小为 2 的函数特化版本,下面给出一个例子。
其中 /* exp */
暂时理解为可转化为任意类型的表达式。当 \(m>2\) 时,会发生替换失败,当 \(m=2\) 时可以正常返回 2
,我们可以定义一个包含任意类型的用户定义转换函数的辅助类来完成 /* exp */
所代表的功能。
该辅助类 init
提供了任意类型的用户定义转换函数,但无需实现,因为用在 decltype
不求值表达式中。
为此,汇总所有信息,可以得到以下代码。
其中涉及到了一个 size_tag
类,该类有一个 <0>
的全特化,<N>
继承于 <N - 1>
,是为了在 helper::size
重载中,能够按 N
从大到小的顺序进行重载决议。下面给出一个简单的使用示例。
C++20 有了概念的机制,可以不通过 SFINAE 机制完成。我们直接上代码
首先描述一下 size
模板函数的实现,它是一个递归函数,并且是编译期的递归函数,因为使用了 if constexpr
,并且这个函数是以 consteval
修饰的,是立即函数,即该函数必须在编译期运行并产生编译期常量,因此没有运行时开销。
requires
语句指明,如果 Tp
可以按照 args...
的方式进行构建,那么就进入 else
分支,否则返回 形参包长度 - 1
。根据上文提到的 性质 2 可以知道,当 Tp
聚合体在初始化时,初始化列表的元素个数如果符合 Tp
构造的要求,即能够从 args...
形参包完成 Tp
的构造,那么在 else
分支中,会利用 C++17 部分提到的辅助类 init
再额外添加一个参数,并进行递归调用。当形参包长度刚好超过了能够构造的长度时,返回值则恰好是能够参与构造的最大长度,即 Tp
的元素个数。
我们通过一个例子来说明,并给出详细的运行步骤。
运行步骤
size
函数,Tp
是 X
类型,形参包 args
为空;constexpr-if
语句中,条件表达式等价于 !requires { Tp{}; }
,符合语法,requires
表达式返回 true
,经 !
修饰后,则会进入 else
分支;else
,此时返回值相当于 size<Tp>(init{});
;size
函数,此时形参包有 \(1\) 个参数 init{}
;constexpr-if
语句中,条件表达式等价于 !requires { Tp{init{}}; }
,符合语法,同步骤 2,进入 else
分支;else
,此时返回值相当于 size<Tp>(init{}, init{});
;size
函数,此时形参包有 \(2\) 个参数 init{}
;constexpr-if
语句中,条件表达式等价于 !requires { Tp{init{}, init{}}; }
,符合语法,同步骤 2,进入 else
分支;else
,此时返回值相当于 size<Tp>(init{}, init{}, init{});
;size
函数,此时形参包有 \(3\) 个参数 init{}
;constexpr-if
语句中,条件表达式等价于 !requires { Tp{init{}, init{}, init{}}; }
,不符合语法(X
只有两个成员),requires
表达式返回 false
,经 !
修饰后,则会进入 true
分支;形参包长度 - 1
,即返回 \(2\)。函数原型如下
其中
Tp
— 聚合类型(必须满足)Callable
— 可调用对象类型,可以是函数模板,函数对象模板,或者 lambda 表达式的简写模板(即 auto &&
类型)下面给出一个例子
编译后运行结果为
实现方法简单粗暴,通过结构化绑定与编译期的 constexpr-if
语句,分别调用可调用对象 f
即可,例如,一个 size = 3
的聚合体,可以使用以下语句完成成员的遍历
operator==
函数来实现同样的功能,可参考默认比较函数原型如下
其中
Tp
— 聚合类型(必须满足)下面给出一个例子
编译后运行结果为
实现方法同样简单粗暴,核心操作与 for_each
的几乎一致,代码如下
cppreference 中给出了有关自定义散列函数的例子
RMVL 提供了聚合类一般化的接口,即任意聚合类的自定义散列函数 rm::hash_aggregate
,基本用法如下
此外,对于一般化的类型 T
,如果要使用 std::unordered_map
,可以使用 类型特性 type traits 相关功能,RMVL 提供了用于 hash 函数选择的类型特性: rm::hash_traits
,类型名为 hash_func
,下面给出一个简单的用法