书接上回,上回完成了数据的二进制转换,在这回需要对结构体的数据成员实现 反射reflection

何为反射?

在其他大多数语言中有反射的机制,这种机制能在运行态时能够获取到一个类中的所有属性和方法的集合,比如 JS 就能通过Object.getOwnPropertyNames() 方法返回其自身属性集合:

var arr = ["a", "b", "c"];
var list = Object.getOwnPropertyNames(arr)
console.log(list)

输出:

['0', '1', '2', 'length']

于是可以使用得到的这个数组进行遍历操作:

list.forEach((item)=>{ console.log(`arr::${item}()`, "-->" , arr1[item]) })

输出:

arr::0() --> a
arr::1() --> b
arr::2() --> c
arr::length() --> 3

这样也不难看出如果反射机制运用在数据绑定和转换中确实会带来许多的便利

如何实现?

反射机制在C++中有两种实现方式:

  • 静态反射:采用模板的方式在编译期间决定
  • 动态反射:类似于 Json 库实现的方式

这里的使用场景只是用于数据的转换,因此采用静态的方法进行实现
在静态反射需要使用的一个很重要的工具类: tuple(元组) 。元组需要在 C++ 中明确元素的类型,这样才能在编译期决定代码的排列,即:

std::tuple<int, char, std::string> _tuple { 1, 'F', "ABCD" };
auto i = std::get<0>(_tuple);
auto c = std::get<1>(_tuple);
auto str = std::get<2>(_tuple);

tuple 的简单分析

由于元组需要在编译器期间决定数据类型,因此需要在在编写期间进行赋值由编译器进行推导或者明确类型交予后续的赋值处理。那么元组的实际组成是什么样的呢?以及它如何进行构成的?

这里的代码分析基于 MSVC 14.40.33807

查看&精简源代码可知:

template <class _Ty>
struct _Tuple_val { // stores each value in a tuple
constexpr _Tuple_val() : _Val() {}

template <class _Other>
constexpr _Tuple_val(_Other&& _Arg) : _Val(_STD forward<_Other>(_Arg)) {}
...
// 被保存的实际值
_Ty _Val;
};

template <class _This, class... _Rest>
class tuple<_This, _Rest...> : private tuple<_Rest...> { // recursive tuple definition
public:
// 保存当前的元素类型
using _This_type = _This;
// 记录基类的元素类型
using _Mybase = tuple<_Rest...>;
...
// 用于保存元素值的成员
_Tuple_val<_This> _Myfirst; // the stored element
};

不难看出其实元组实际上是通过递归继承自我的方式来实现的,而且最底层的构成并不是十分复杂,其他的基本是对接标准库接口或者自身的 API:

classDiagram
direction RL
    class `_Tuple_val<_Ty>` {
        +_Ty _Val;
        +...()
    }
    class `tuple<_This, _Rest...>`{
        +_Tuple_val<_This> _Myfirst;
        +...()
    }
    `tuple<_This, _Rest...>` *-- `_Tuple_val<_Ty>`
    `tuple<_This, _Rest...>` <|-- `tuple<_This, _Rest...>`

std::get 是如何拿到元素值的呢?查看对应的源码:

template <class _This, class... _Rest>
struct _MSVC_KNOWN_SEMANTICS tuple_element<0, tuple<_This, _Rest...>> { // select first element
using type = _This;
// MSVC assumes the meaning of _Ttype; remove or rename, but do not change semantics
using _Ttype = tuple<_This, _Rest...>;
};

template <size_t _Index, class _This, class... _Rest>
struct _MSVC_KNOWN_SEMANTICS tuple_element<_Index, tuple<_This, _Rest...>>
: tuple_element<_Index - 1, tuple<_Rest...>> {}; // recursive tuple_element definition

_EXPORT_STD template <size_t _Index, class... _Types>
_NODISCARD constexpr tuple_element_t<_Index, tuple<_Types...>>& get(tuple<_Types...>& _Tuple) noexcept {
using _Ttype = typename tuple_element<_Index, tuple<_Types...>>::_Ttype;
return static_cast<_Ttype&>(_Tuple)._Myfirst._Val;
}

可以看出对应的 get 在知晓传入元组的类型后调用了 tuple_element 结构体来对传入的下标进行递归减少,当 _Index 为 0 时取得对应的元素类型,然后对传入的元组进行转换拿到对应的元素值。同时也可以看出编译器会强制要求元组的顺序性,保证 get 返回正确数据

实现

对于需要进行数据绑定和转换的结构体来说,可以指定一个元组来保存所需要成员的相关参数,这里列举三个:

  1. 类型
  2. 名称
  3. 地址

因此需要定义个结构体来进行保存:

/**
* 属性(成员)元数据
* @tparam Class 类/结构体
* @tparam T 成员类型
*/
template <typename Class, typename T>
struct PropertyMeta {
/**
* 编译器构造函数
* @param member 成员地址
* @param name 成员名
*/
constexpr PropertyMeta(T Class::*member, const char* name)
: Name(name),
Member(member) {}
// 类型
using Type = T;
// 成员名称
const char* Name;
// 成员地址
T Class::*Member;
};

定义了之后就可以使用 tuple 来保存所需要处理的成员:

struct A {
int num;
char txt;

static constexpr auto Tuple = std::make_tuple(
tools::PropertyMeta<A, int>(&A::num, "num"),
tools::PropertyMeta<A, char>(&A::txt, "txt"));
};

int main() {
A a {
.num = 1,
.txt = 'F'
};
// 打印输出两个成员
std::cout << "(" << std::get<0>(A::Tuple).Name << ", "
<< a.*(std::get<0>(A::Tuple).Member) << ")" << std::endl; // (num, 1)
std::cout << "(" << std::get<1>(A::Tuple).Name << ", "
<< a.*(std::get<1>(A::Tuple).Member) << ")" << std::endl; // (txt, F)
}

同时为了便于使用,还是需要定义合适的函数模板以及宏,因此这一块的最终实现如下:

namespace tools {
// 用于生成属性元组的区域
#define VAR_PROPERTY_SCOPE(CLASS, ...) \
using __Var = CLASS; \
static constexpr auto __Property = std::make_tuple(__VA_ARGS__);

// 默认方式
#define VAR_PROPERTY(MEMBER) \
tools::PropertyMeta(&Elem::MEMBER, #MEMBER)

// 自定义成员名称
#define VAR_PROPERTY_SCHEME(MEMBER, MEMBER_NAME) \
tools::PropertyMeta(&Elem::MEMBER, MEMBER_NAME)

namespace _impl {
/**
* 属性(成员)元数据
* @tparam Class 类/结构体
* @tparam T 成员类型
*/
template <typename Class, typename T>
struct PropertyMeta {
/**
* 编译器构造函数
* @param member 成员地址
* @param name 成员名/字段
*/
constexpr PropertyMeta(T Class::*member, const char* name)
: Name(name),
Member(member) {}
// 类型
using Type = T;
// 成员名称
const char* Name;
// 成员地址
T Class::*Member;
};
} // _impl

/**
* 生成对应属性(成员)元数据
* @tparam Class 类/结构体
* @tparam T 成员类型
* @param member 成员地址
* @param name 成员名/字段
* @return 返回元数据
*/
template <typename Class, typename T>
constexpr auto PropertyMeta(T Class::*member, const char* name) {
return _impl::PropertyMeta<Class, T>(member, name);
}
} // tools

因此在结构体中调用就可以变成

struct A {
int num;
char txt;
static constexpr auto Tuple = std::make_tuple(
tools::PropertyMeta<A, int>(&A::num, "num"),
tools::PropertyMeta<A, char>(&A::txt, "txt"));
};
struct A {
int num;
char txt;
VAR_PROPERTY_SCOPE(A,
VAR_PROPERTY(num),
VAR_PROPERTY(txt))
};

那么现在有个问题,如何对元组进行遍历呢?毕竟无法从元组中获取对应的个数信息,然后通过 get 来进行依次获取。对于这个问题在不同的版本中有各自对应的解决方案,这里使用 C++17 中的 std::apply 来解决:

#if _HAS_CXX17
// TODO: Support other version
/**
* 遍历属性数据
* @tparam Args 元素类型
* @tparam Handler 处理函数类型
* @param tuple 元组
* @param handler 处理函数回调
*/
template<typename... Args, typename Handler>
void PropertyForeach(const std::tuple<Args...>& tuple, Handler&& handler) {
std::apply([&](const Args&... args) {
(handler(args), ...);
}, tuple);
}
#endif

有了上面这些处理后,对于正反序列化来说就可以更进一步的完善:

/**
* 序列化
* @tparam T 值类型
* @param buffer 目标空间
* @param value 待转换值
*/
template<typename T>
void Serialized(_impl::buffer::Type &buffer, const T &value) {
// 遍历属性中的成员数据, 按照顺序进行转换
PropertyForeach(T::__Property, [&](auto elem) {
_impl::ToBuffer<typename decltype(elem)::Type>()(buffer, value.*(elem.Member));
});
}

/**
* 反序列化
* @tparam T 目标类型
* @param value 目标值地址
* @param buffer 带转换内存空间
* @return 转换成功与否
*/
template<typename T>
bool Deserialized(T &value, const _impl::buffer::Type &buffer) {
int offset = 0;
try {
// 遍历属性中的成员数据, 按照顺序进行转换
PropertyForeach(T::__Property, [&](auto elem) {
auto len = _impl::FromBuffer<typename decltype(elem)::Type>()(value.*(elem.Member), buffer, offset);
// 解析出错时及时抛出异常结束遍历
if (len < 0) throw std::logic_error("tools::cvt => bad parsed");
offset += len;
});
}
catch (...) {
return false;
}
return true;
}

这样就实现了对结构体的成员进行数据绑定和转换

测试

同上节,也是进行来回转换进行比对:

struct Complex {
int8_t int_8;
int16_t int_16;
int32_t int_32;
int64_t int_64;
float f_4;
double f_8;
std::string str;
std::vector<int> int_arr;
std::vector<std::string> str_arr;

VAR_PROPERTY_SCOPE(Complex,
VAR_PROPERTY(int_8),
VAR_PROPERTY(int_16),
VAR_PROPERTY(int_32),
VAR_PROPERTY(int_64),
VAR_PROPERTY(f_4),
VAR_PROPERTY(f_8),
VAR_PROPERTY(str),
VAR_PROPERTY(int_arr),
VAR_PROPERTY(str_arr)
)
};

template <typename T>
bool CvtComplexTest(const T& val, T& result) {
std::vector<uint8_t> buffer;
// 不为struct/class则返回false
// TODO: !!!无法判断基本的容器类型
if constexpr (!std::is_class_v<T>) {
return false;
}
tools::cvt::Bianry::Serialized(buffer, val);
return tools::cvt::Bianry::Deserialized(result, buffer);
}