由于 cpp 还未提供反射,所以一般项目里序列化里需要实现对应类的序列化,不仅繁琐还容易出错,使用宏也并没有本质差别,都是侵入式的序列化。最近看 yalantinglibs 库中 struct_pack 的反射非常有意思,很简单的一些代码就可以实现反射。另外这个库的很多实现很 tricky 可以仔细阅读。
为了便于理解本文将简化一下实现。
对于一个类
struct Foo {
int n;
string str;
};
如果我们想对该类的对象进行序列化,在没有反射的情况下,需要由用户来手动来遍历该类的成员,例如
struct Foo {
int n;
string str;
friend Serializer& operator >> (Serializer& s, Foo& f) {
s >> f.n >> f.str;
return s;
}
friend Serializer& operator << (Serializer& out, Foo& f) {
s << f.n << f.str;
return s;
}
};
这样就可以实现 Foo 类的序列化和反序列化。但正如前文所说,这样不仅繁琐还容易出错。
我们希望实现一个非侵入式的序列化,用户只要定义类就行,由框架来完成遍历类的成员并完成序列化和反序列化,直接做到以下效果,这就是反射的作用
Foo foo;
Serializer s;
// 序列化
s << foo;
// 反序列化
s >> foo;
首先最为核心的地方,要想获取类的全部成员,在 cpp 17 里有个简单的方法,就是结构化绑定(structured binding)。
Foo foo;
auto &&[a1, a2] = foo;
现在 a1 就是对 foo.n 的引用,a2 就是对 foo.str 的引用。
简单封装一下,我们需要定义一个高阶函数 VisitMembers,实现 Vistor 模式,其接受两个参数:
反射的对象 auto&& object
一个函数 visitor,对对象全部字段进行访问、操作,签名为 void(auto &&...items)
,其中参数为变参模板
constexpr decltype(auto) VisitMembers(auto &&object, auto &&visitor) {
using type = std::remove_cvref_t<decltype(object)>;
constexpr auto Count = MemberCount<type>();
...
if constexpr (Count == 0) {
return visitor();
}
else if constexpr (Count == 1) {
auto &&[a1] = object;
return visitor(a1);
}
else if constexpr (Count == 2) {
auto &&[a1, a2] = object;
return visitor(a1, a2);
}
else if constexpr (Count == 3) {
auto &&[a1, a2, a3] = object;
return visitor(a1, a2, a3);
}
...
}
代码实现里一直暴力枚举下去。
VisitMembers 里先获取类的成员数量,然后利用 if constexpr 来编译期生成对应成员数量的结构化绑定,将全部成员转发给 visitor,这就完成了对对象成员的访问。
到目前为止都很简单,但有个问题,MemberCount 获取类的成员数量该如何实现,这也是最为魔法的地方。
MemberCount 的真正实现是 MemberCountImpl
template <typename T>
consteval std::size_t MemberCount() {
...
return MemberCountImpl<T>();
}
MemberCountImpl 实现如下
struct UniversalType {
template <typename T>
operator T();
};
template <typename T, typename... Args>
consteval std::size_t MemberCountImpl() {
if constexpr (requires {
T {
{Args{}}...,
{UniversalType{}}
};
}) {
return MemberCountImpl<T, Args..., UniversalType>();
} else {
return sizeof...(Args);
}
}
要想理解这个函数必须先理解这个 concept 约束了什么。
这里涉及到了一个特性
struct Foo {
int a;
int b;
int c;
};
对于一个聚合类 Foo,以下初始化方法都是合法的
Foo a{1};
Foo a{1, 2};
Foo a{1, 2, 3};
concept 里借助了一个万能类型 UniversalType,UniversalType 中有一个可以隐式转换成任意类的稻草人函数。然后将所有的变参 UniversalType 展开检查初始化类 T 时的参数个数是否合法。
第一个分支通过不断构造新的 UniversalType,当 concept 不满足时,说明当前参数的个数就等于类的成员数量。
template<typename T>
Serializer& operator << (const T& i){
using T = std::remove_cvref_t<Type>;
static_assert(!std::is_pointer_v<T>);
if constexpr(std::is_same_v<T, bool> || std::is_same_v<T, char> || std::is_same_v<T, unsigned char>){
m_byteArray->writeFint8(t);
} else if constexpr(std::is_same_v<T, float>){
m_byteArray->writeFloat(t);
} else if constexpr(std::is_same_v<T, double>){
m_byteArray->writeDouble(t);
} else if constexpr(std::is_same_v<T, int8_t>){
m_byteArray->writeFint8(t);
} else if constexpr(std::is_same_v<T, uint8_t>){
m_byteArray->writeFuint8(t);
} else if constexpr(std::is_same_v<T, int16_t>){
m_byteArray->writeFint16(t);
} else if constexpr(std::is_same_v<T, uint16_t>){
m_byteArray->writeFuint16(t);
} else if constexpr(std::is_same_v<T, int32_t>){
m_byteArray->writeInt32(t);
} else if constexpr(std::is_same_v<T, uint32_t>){
m_byteArray->writeUint32(t);
} else if constexpr(std::is_same_v<T, int64_t>){
m_byteArray->writeInt64(t);
} else if constexpr(std::is_same_v<T, uint64_t>){
m_byteArray->writeUint64(t);
} else if constexpr(std::is_same_v<T, std::string>){
m_byteArray->writeStringVint(t);
} else if constexpr(std::is_same_v<T, char*>){
m_byteArray->writeStringVint(std::string(t));
} else if constexpr(std::is_same_v<T, const char*>){
m_byteArray->writeStringVint(std::string(t));
} else if constexpr(std::is_enum_v<T>){
m_byteArray->writeInt32(static_cast<int32_t>(t));
} else if constexpr(std::is_class_v<T>) {
static_assert(std::is_aggregate_v<T>);
VisitMembers(t, [&](auto &&...items) {
(void)((*this) << ... << items);
});
}
return *this;
}
在最后一个if constexpr 里,判断是否为聚合类,然后遍历所有的成员进行序列化。
当然,由于不是原生的反射,还是有许多缺陷,比如无法对一个非聚合类进行自动序列化,此时依旧可以通过模板特化来手动实现,例如
class Foo {
private:
int a;
int b;
public:
friend Serializer& operator >> (Serializer& s, Foo& f) {
s >> f.n >> f.str;
return s;
}
friend Serializer& operator << (Serializer& out, Foo& f) {
s << f.n << f.str;
return s;
}
};
虽然通过模拟反射,我们完成了无侵入式序列化的目的,但毕竟是模拟出来的,能获取到的元数据及其匮乏,实现起来也蹩脚。
期望在不久的将来能看到 cpp 提供足够的反射信息。