C++技法:模板元编程编译期获取类成员数量
C++反射中,有个必要的就是需要获取一个类的成员个数,然后就可以根据个数,将类的成员通过std::tie转换成tuple。继而可以实现equal、hash、serialize等功能。
基本原理
本文介绍分析如何获取一个类的成员个数的方法,通过模板元编程实现,基本原理:
+ 编译时计算结构体成员数量
核心代码实现
核心代码如下:
| C++ | 
|---|
|  | #pragma once
#include <type_traits>
namespace detail {
// instance类的作用:定义了operator函数,提供到任意类型的隐式转换操作符,用于模拟构造Aggregate类型时所需的任意类型参数
struct instance {
  template <typename Type>
  operator Type() const;
};
template <typename Aggregate, typename IndexSequence = std::index_sequence<>,
          typename = void>
struct arity_impl : IndexSequence {};
// 特化版本
template <typename Aggregate, std::size_t... Indices>
struct arity_impl<Aggregate, std::index_sequence<Indices...>,
                  std::void_t<decltype(Aggregate{
                      (static_cast<void>(Indices), std::declval<instance>())...,
                      std::declval<instance>()})>>
    : arity_impl<Aggregate,
                 std::index_sequence<Indices..., sizeof...(Indices)>> {};
}  // namespace detail
template <typename T>
constexpr std::size_t arity() noexcept {
  // 使用decay_t去除类型修饰(如const/volatile/引用)
  return detail::arity_impl<decay_t<T>>().size();
}
 | 
首先,对上面的代码进行拆解,分细节进行讨论:
细节1: 构造Aggregate对象+逗号表达式
| C++ | 
|---|
|  | Aggregate{
    (static_cast<void>(Indices), std::declval<instance>())...,
    std::declval<instance>()}
 | 
| C++ | 
|---|
|  | (static_cast<void>(Indices), std::declval<instance>())
 | 
C++中的逗号表达式是一种特殊的运算符,它可以将多个表达式连接起来并按顺序求值。逗号表达式的一般形式为:表达式1, 表达式2, …, 表达式n。其求值过程是从左到右依次计算每个子表达式,最终整个表达式的值为最后一个表达式(表达式n)的值。例如:
| C++ | 
|---|
|  | int a = (1, 2, 3); // a的值为3
 | 
参数包(Indices, instance)…生成N个instance,额外添加的instance用于探测边界条件。也就是indices为(0, 1)时,传递3个参数,当indices为(0, 1, 2)时,传递4个参数。
细节2:SFINAE技术+void_t表达式合法性检查
| C++ | 
|---|
|  | std::void_t<decltype(Aggregate{
                      (static_cast<void>(Indices), std::declval<instance>())...,
                      std::declval<instance>()})>>
 | 
说明一下这里std::void_t,void_t通常结合SFINAE技术进行元编程的类型诊断与表达式的合法性检查,void_t本身定义非常简单,对于任意类型,乃至可变的参数类型,都重定义为void。
看个例子你就明白了:
- 检测类型成员是否存在
| C++ | 
|---|
 |  | // 泛化版本(默认返回 false)
template<typename T, typename = void>
struct has_type_member : std::false_type {};
// 特化版本(当 T::type 存在时匹配)
template<typename T>
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type {};
 |  
 
若 T 包含 type 成员类型,则特化版本生效,返回 true。
+ 检测函数是否存在
| C++ | 
|---|
|  | template<typename T, typename = void>
struct has_hello_func : std::false_type {};
template<typename T>
struct has_hello_func<T, std::void_t<decltype(std::declval<T>().hello())>> 
    : std::true_type {};
 | 
细节3:index_sequence的使用
std::index_sequence是C++14引入的编译期整数序列工具,主要用于模板元编程中处理参数包和索引操作。
例如:
| C++ | 
|---|
|  | template<size_t... I>
constexpr auto make_squares(std::index_sequence<I...>) {
    return std::array{I*I...};
}
auto arr = make_squares(std::make_index_sequence<5>{}); // {0,1,4,9,16}
 | 
细节4: 递归探测的机制
| C++ | 
|---|
|  | template <typename Aggregate, std::size_t... Indices>
struct arity_impl<Aggregate, std::index_sequence<Indices...>,
                  // 本次探测的参数列表,用于探测Aggregate能接受的参数数量,参数数量为sizeof...(Indices)+ 1
                  std::void_t<decltype(Aggregate{
                      (static_cast<void>(Indices), std::declval<instance>())...,
                      std::declval<instance>()})>>
    // 递归探测,每次递归增加一个参数,增加的参数为sizeof...(Indices)
    : arity_impl<Aggregate,
                 std::index_sequence<Indices..., sizeof...(Indices)>> {};
 | 
递归探测机制:
+ 从空参数列表开始(std::index_sequence<>)
- 
每次递归增加一个参数(std::index_sequence<Indices…, sizeof…(Indices)>),比如原来是:std::index_sequence<>,下一次递归变为std::index_sequence<0>,再下一次递归变为std::index_sequence<0, 1>,以此类推。 
- 
当参数数量超过Aggregate类型成员数量时,SFINAE使特化版本失效 
通过一个例子理解计算过程
arity<T>()函数模板可以返回任意类型T的成员数量,完全在编译期计算,零运行时开销。
| C++ | 
|---|
|  | struct Point { int x; double y; };
// 尝试构造:
Point{
    instance{},  // 匹配x
    instance{},  // 匹配y 
    instance{}   // 触发SFINAE (Point只有2个成员)
}
 | 
当参数数量=3时构造失败,递归终止,确定arity=2。
以结构体Point为例,逐步解释这个模板特化的执行过程:
- 第一步, 尝试匹配特化版本(带std::void_t的版本)
| C++ | 
|---|
|  | arity_impl<Point, std::index_sequence<>>
 | 
| C++ | 
|---|
|  | arity_impl<Point, std::index_sequence<0>>
 | 
| C++ | 
|---|
|  | Point{instance{}, instance{}}
 | 
| C++ | 
|---|
|  | arity_impl<Point, std::index_sequence<0,1>>
 | 
尝试构造:
| C++ | 
|---|
|  | Point{instance{}, instance{}, instance{}}
 | 
构造失败(超过2个参数), 递归终止。
最终继承arity_impl<Point, std::index_sequence<0,1>>, size()返回2。
| C++ | 
|---|
|  | Point p1 = {};
Point p2 = {1};
Point p3 = {1, 2};
Point p4 = {1, 2, 3}; // error, 超过2个参数
 | 
通过这个例子,我们可以看到,通过递归探测,可以确定任意类型的成员数量。
具体使用:
| C++ | 
|---|
|  | int main(){
  int nums = cista::arity<Point>();
  std::cout << nums << std::endl;
  return 0;
}
 |