反射实现无侵入式序列化

引言

由于 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 模式,其接受两个参数:

  1. 反射的对象 auto&& object

  2. 一个函数 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 提供足够的反射信息。

你可能感兴趣的:(c++)