C++反射原理探究(第二部分)

Yes and no

Posted by zyl on March 11, 2024

又是一次拖了很久的更新,不过,拖更是情有可原的:因为出了一次大长差(为期一年),同时这份差事性质比较特殊(有点小偏僻同时也不允许使用网络),所以“失联”了一段时间。客观因素导致堆了一肚子的东西没法吐露出来。而且在经历了一年的断网原始人生活回来后,真的体会到了“沧海桑田”一词的分量。好的,废话不多说。开始今天的内容。

在之前的反射原理探究中,我们实现了类型萃取与静态反射部分,同时挖了一个坑,那么就是同样重要的动态反射部分。今天来填掉之前挖的坑。

一、变量类型信息体

和静态反射相同,我们首先得保有存储类型信息的部分。不过请放心,这个动态反射实现里面并不会涉及到太多的模板元编程的黑魔法。会更加回归到OOP的本质:类与对象。既然如此,那么我们就要对每种类型创建一个描述他们的信息类。让我们从最简单的变量开始。

首先可以知道的是,变量类型一般有三种:适合托管类型语言的Bittable Byte(如int,long,float,double等,我们也可以把它叫做数值类型)、枚举类型、自定义结构体(或者类)。那么我们就可以根据以上三种分类来创建变量类型结构体。不过首先让我们创建一个基类Type,用以存储所有类型信息中共用的部分:

// 类型基类定义
class Type
{
public:

    // 这里用来指示类实例对应的变量类型大致种类
    // 共三种:数值、枚举、结构体(或类)
    enum class TypeEnum
    {
        NUM,
        ENUM,
        CLASS
    };

    // 动态反射中比较常用的主要设计模式便是工厂模式
    // 这里将生产枚举类型信息的工厂以及生产结构体类型信息类型的工厂设为友元类型
    template<typename T>
    friend class EnumFactory;

    template<typename T>
    friend class ClassFactory;

    // 当然了,析构函数肯定是要virtual的
    virtual ~Type() = default;

    // 构造函数中填入的参数就两个:类型名、类型枚举
    Type(const std::string& name, TypeEnum type):
        m_typeName(name),
        m_type(type) {}
    
    // 类型名与类型枚举的Get函数,不做过多赘述
    const std::string GetTypename() const
    {
        return m_typeName;
    }
    const TypeEnum GetTypeEnum() const
    {
        return m_type;
    }
    
private:
    std::string m_typeName;
    TypeEnum m_type;
};

接下来开始完成数值类型的信息体,在Type基类中我们只需要知道实例对应三种枚举的哪一个即可。但在细分的部分,比如常见数值类型会有多种:int、short、long、float、double等等。所以,数值类型的信息体内需要存储更细分的枚举类型。如下所示:

enum class Kind
{
    UNKNOWN = 0,                    // 未知类型,缺省值
    I8,                             // 8位长整数,char、bool等类型
    I16,                            // 16位长整数,short
    I32,                            // 32位长整数,int
    I64,                            // 64位长整数,long
    I128,                           // 128位长整数,long long
    FLOAT,                          // 单精度浮点数
    DOUBLE                          // 双精度浮点数
};

根据以上定义我们就可以确定存入信息体内的具体是哪种类型,那么信息体的实现就由一大堆的Get以及Set函数组成:

// 数值类型信息体,继承自类型信息体基类
class Numeric : public Type
{
public:
    // 详细类型枚举
    enum class Kind
    {
        UNKNOWN = 0,
        I8,
        I16,
        I32,
        I64,
        I128,
        FLOAT,
        DOUBLE
    };

    // 构造函数,传入符号标识符以及数据类型即可,基类的构造方法由GetTypeName方法实现
    Numeric(Kind typeKind, bool isSigned):
        Type(GetTypeName(typeKind), TypeEnum::NUM),
        m_isSigned(isSigned),
        m_typeKind(typeKind){}

    // 符号标识符以及数据类型的Get方法
    const Kind GetTypeKind() const noexcept { return m_typeKind; }

    const bool IsSigned() const noexcept { return m_isSigned; }

    // 通过调用此静态函数获取一个数值类型信息体的实例
    template <typename T>
    static Numeric CreateNumeric()
    {
        // 这里使用了标准库中的is_signed_v模板变量判断是否带符号
        // 同时在私有方法中定义了DetectType方法,同来确定填入的类型
        // 由于实现的原理不同,动态反射无法像静态反射一样直接使用模板元编程获取类型信息并存储
        return Numeric(DetectType<T>(), std::is_signed_v<T>);
    }

private:
    Kind m_typeKind;
    bool m_isSigned;

    // 根据填入的枚举类型获取类型名称字符串
    static std::string GetTypeName(Kind kind)
    {
        switch (kind)
        {
        case Kind::I8:
            return "int8";
            break;
        case Kind::I16:
            return "int16";
            break;
        case Kind::I32:
            return "int32";
            break;
        case Kind::I64:
            return "int64";
            break;
        case Kind::I128:
            return "int128";
            break;
        case Kind::FLOAT:
            return "float";
            break;
        case Kind::DOUBLE:
            return "double";
            break;
        default:
            return "Uknown";
            break;
        }
    }

    // 通过使用标准库中的is_same_v模板变量进行类型比较以确定类型枚举值
    template <typename T>
    static Kind DetectType()
    {
        if constexpr (std::is_same_v<T, char>) { return Kind::I8; }
        else if constexpr (std::is_same_v<T, short>) { return Kind::I16; }
        else if constexpr (std::is_same_v<T, int>) { return Kind::I32; }
        else if constexpr (std::is_same_v<T, long>) { return  Kind::I64; }
        else if constexpr (std::is_same_v<T, long long>) { return Kind::I128; }
        else if constexpr (std::is_same_v<T, float>) { return Kind::FLOAT; }
        else if constexpr (std::is_same_v<T, double>) { return Kind::DOUBLE; }
        else { return Kind::UNKNOWN; }

    }
};

接下来是枚举类型信息体的实现,与数值方式不同的是,枚举类型不需要再对类型进行细分。我们只需要存储枚举标识和枚举值的一一对应键值对即可。那么我们可以很简单的实现枚举类型信息体:

// 枚举类型信息体实现,继承自类型信息体基类
class Enum : public Type
{
public:

    // 默认构造函数(缺省)
    Enum() : Type("Unreg_Enum", TypeEnum::ENUM){}
    // 注册用构造函数,参数为待注册枚举类型的名称字符串
    Enum(std::string enumName) : Type(enumName, TypeEnum::ENUM) 
    {

    }

    // 用于存储键值对的结构体,这里考虑到使用数字范围的问题,故使用long long进行存储
    // 具体该使用哪种长度整型存储,依据使用情况自己定义即可
    struct EnumItem
    {
        using ItemType = long long;
        std::string m_itemName;
        ItemType m_itemVal;
    };

    // 添加键值对
    // 这里使用模板类型的原因是,由于不能保证传入的枚举值一定是long long,
    // 所以这里通过静态转换将可能填入的值强制转换为long long类型
    template <typename T>
    void AddEnumItem(std::string name, T val)
    {
        // 这里使用emplace_back方法保证插入键值对的唯一性
        m_items.emplace_back(EnumItem{name, static_cast<typename EnumItem::ItemType>(val)});
    }

    // 返回键值对数组
    const std::vector<EnumItem> GetItems() const
    {
        return m_items;
    }

private:
    std::vector<EnumItem> m_items;
};

二、类类型信息体

在开始之前,我们需要先实现成员类型信息体,或者也可以称为域类型信息体(是不是很耳熟?)。根据我们在静态反射中的经验,域类型信息体内肯定是要存放有类型信息的,不过鉴于函数类型中有可能存在一个或多个类型信息,那么也就是说与类型信息体与数值类型信息体之间的关系应该是“has-a”关系,而不再是和静态反射一样的“is-a”关系了(虽然从编译器的角度来说C++中的has-a以及is-a没有太大区别,几乎都会视为has-a,这也就是为什么C++支持多继承的原因)。知道了这些前置条件后,那么就可以开始着手于相关域类型信息体的实现,首先从比较简单的字段域类型信息体开始:

struct MemberVariable
{
    // 类型名称和类型信息体指针
    // 类型信息提指针所指向的内容由类类型信息体提供
    std::string m_varName;
    const Type* m_varType;

    // 创建一个类型的信息体实例的静态方法
    // 这里的GetType先不做解释,后续会有说明
    // 仅需要明白一点,我们可以通过之前提到的类型萃取获取到我们需要的类型信息并传入这个函数,这个函数可以返回我们需要的Type信息体
    template <typename T>
    static MemberVariable CreateType(const std::string& name)
    {
        using VarTypeTraits = variable_traits<T>;
        return MemberVariable{name, GetType<VarTypeTraits>()};
    }
};

由于方法域类型内至少会包含一个乃至多个类型信息,即返回值类型、各个参数类型等,所以在实现上和字段类型略有不同:

struct MemberFunction
{
    // 类型名称和类型信息体指针
    // 类型信息提指针所指向的内容由类类型信息体提供
    std::string m_funcName;
    const Type* m_funRetType;
    // 各个参数类型信息
    std::vector<const Type*> m_funcParamType;

    // 创建一个类型的信息体实例的静态方法
    template <typename T>
    static MemberFunction CreateType(const std::string& name)
    {
        using traits = function_traits<T>;
        using args = typename traits::function_variables_basic_type;
        return MemberFunction{
                name, 
                GetType<CppMirror::function_traits<T>::function_return_type>(),
                TypeLst2Vec<args>(std::make_index_sequence<std::tuple_size_v<args>>())
            };
    }

    // 将tuple元组转成Type指针vector数组方法,通过可变参数模板以及标准库的sequence完成,index_sequence提供各元组元素对应索引,通过解包可变参数包以及索引值存入vector内的对应的位置
    template <typename T, size_t... Idx>
    static std::vector<const Type*> TypeLst2Vec(std::index_sequence<Idx...>)
    {
        return {GetType<std::tuple_element_t<Idx, T>>()...};
    }
};

在以上均完成后,就可以开始真正的类类型信息体的构建:

// 类类型信息体声明,继承自Type类型信息体基类
class Class: public Type
{
public:
    Class() : Type("Unreg_Class", TypeEnum::CLASS){}
    
    Class(std::string className) : Type(className, TypeEnum::CLASS) 
    {

    }

    // 添加域类型信息体实例
    void AddMemberVar(MemberVariable&& var)
    {
        m_vars.emplace_back(std::move(var));
    }
    void AddMemberFunc(MemberFunction&& func)
    {
        m_funcs.emplace_back(std::move(func));
    }

    // 获取字段域以及方法域的类型信息体数组
    auto& GetVariables()
    {
        return m_vars;
    }

    auto& GetFuncs()
    {
        return m_funcs;
    }

private:
    std::vector<MemberVariable> m_vars;
    std::vector<MemberFunction> m_funcs;
};

进行到这里,其实每种域类型都缺少了一个比较关键的信息:访问权限修饰符。这里其实也是一个“三角形边界值”的多选择问题,这里可以选择把注册类内的所有成员域一股脑地都存进去,也可以根据访问权限修饰符类型存储,比如只存public修饰的公有成员域。

有一个可以参照的实现是.NET,C#中的反射选择的是前者,即一股脑全存进去。说起来有意思的是,见过一些C#方面的面试题,其中有一个就是关于反射方面的:“类的扩展方法能否访问已经定义的私有成员?”,实际上,在外部实现扩展方法终究还是在类定义的外部,这一点上无论是C++还是C#是一致的,不能访问私有成员。但有趣的地方来了,C#的扩展方法特性就是依赖于反射系统实现,结果是可以完全通过本类的成员域引用以及实例引用来访问甚至修改私有成员。所以如果后续再碰到这类问题,感觉完全可以使用经典英剧《是,大臣》里汉弗莱的一句传世名言概括回答:“Yes and no”(本期博文副标题)。既可以访问,又不可以访问。

接下来在继续之前,需要先对基础类型信息体基类进行一点小小的修改,添加如下几个转换到特定信息提的转换函数:

// 转换到数值类型
Numeric* Cast2Numeric() const
{
    if(m_type == TypeEnum::NUM)
    {
        return (Numeric*)(this);
    }
    return nullptr;
}
// 转换到枚举类型
Enum* Cast2Enum() const
{
    if(m_type == TypeEnum::ENUM)
    {
        return (Enum*)(this);
    }
    return nullptr;
}
// 转换到类类型
Class* Cast2Class() const
{
    if(m_type == TypeEnum::CLASS)
    {
        return (Class*)(this);
    }
    return nullptr;
}

特别注意,由于基类的定义要先于其他类型,所以各类型需要在基类前进行前向声明。

三、类型工厂

由于各个类型的工厂只负责生产与获取类型信息体,所以这里直接放出各个类型工厂的定义。可以参阅注释查看

// 数值类型信息体的工厂
template <typename T>
class NumericFactory final
{
public:
    // 获取工厂实例
    static NumericFactory& GetInstance()
    {
        static NumericFactory inst{Numeric::CreateNumeric<T>()};
        return inst;
    }

    // 获取生产的信息体对象
    const Numeric& GetInfo()
    {
        return m_info;
    }

private:
    Numeric m_info;

    NumericFactory(Numeric&& info): m_info(std::move(info)){}
};

// 枚举类型信息体工厂
template <typename T>
class EnumFactory final
{
public:
    // 获取工厂实例
    static EnumFactory& GetInstance()
    {
        static EnumFactory inst;
        return inst;
    }

    // 获取生产的信息体对象
    const Enum& GetInfo()
    {
        return m_info;
    }

    // 由于和数值类型不同,具体类型名是由开发人员自定义的,所以这里得手动输入
    // 后面的类类型也一样
    EnumFactory& RegistEnum(const char* typeName)
    {
        m_info.m_typeName = typeName;
        return *this;
    }

    // 填入自定义枚举键值
    template <typename T>
    EnumFactory& AddItems(const char* itemName, T val)
    {
        m_info.AddEnumItem(itemName, val);
        return *this;
    }
private:
    Enum m_info;
};

// 类类型信息体工厂
template <typename T>
class ClassFactory final
{
public:
    // 获取工厂实例
    static ClassFactory& GetInstance()
    {
        static ClassFactory inst;
        return inst;
    }

    // 获取信息体实例
    const Class& GetInfo()
    {
        return m_info;
    }

    // 以下三个方法分别为类名注册、类成员字段注册以及类方法注册
    ClassFactory& RegistClass(const char* typeName)
    {
        m_info.m_typeName = typeName;
        return *this;
    }

    template <typename T>
    ClassFactory& AddVars(const char* varName)
    {
        m_info.AddMemberVar(MemberVariable::CreateType<T>(varName));
        return *this;
    }

    template <typename T>
    ClassFactory& AddFunc(const char* funcName)
    {
        m_info.AddMemberFunc(MemberFunction::CreateType<T>(funcName));
        return *this;
    }

private:
    Class m_info;
};

// 缺省值工厂,用于未知类型或不受支持类型的工厂
// 在类型不受反射框架支持时,会在编译期间报错反馈
class FutureFactory
{
public:
    static FutureFactory& GetInstance()
    {
        static FutureFactory inst;
        return inst;
    }
};

四、动态反射

在实现了各个类型信息体以及相应的工厂之后,那么就可以正式开始动态反射部分的构建了。首先,先完善基础类型信息体的工厂:

template <typename T>
class TypeFactory final
{
public:
    // 说实在的,这其实就可以看作一个静态函数的套壳,通过标准库提供的比较模板
    // 可以确定需要调用的工厂实例,即可在只填入类型参数且不必关心各类型工厂的情况下获取相应工厂以及信息体。
    static auto& GetFactory()
    {
        using No_Ref_Type = std::remove_reference_t<T>;
        using No_Ptr_Type = std::remove_pointer_t<T>;
        if constexpr (std::is_fundamental_v<T>)
        {
            return NumericFactory<T>::GetInstance();
        }
        else if constexpr (std::is_enum_v<T>)
        {
            return EnumFactory<T>::GetInstance();
        }
        else if constexpr (std::is_class_v<T>)
        {
            return ClassFactory<T>::GetInstance();
        }
        else
        {
            return FutureFactory::GetInstance();
        }
    }
};

接下来就是注册部分以及前文出现的GetType的实现了,首先对GetType的实现做一个说明:获取相应类型工厂实例内的信息体。如下:

template <typename T>
const Type* GetType()
{
    return &TypeFactory<T>::GetFactory().GetInfo();
}

注册部分同样也很好理解:

template <typename T>
auto& RegistType()
{
    return TypeFactory<T>::GetFactory();
}

通过返回工厂实例从而实现对类型的链式调用。

以下举一个实际例子来说明:

class Cl
{
public:

    float func()
    {
        return 0;
    }

    void funcB()
    {
        std::cout << "No Param Done!\n";
    }

    int k = 0;
};

int main()
{
    // 动态反射注册
    RegistType<Cl>().
        RegistClass("Cl").
        AddVars<decltype(&Cl::k)>("Cl::k").
        AddFunc<decltype(&Cl::func)>("func").
        AddFunc<decltype(&Cl::funcB)>("funcB");
}

由于注册的每一步都实现了工厂实例的返回,所以可以以链式调用的方式进行注册。现在说明一下各个木块之间的调用:首先,Registype调用Cl类型的工厂实例,但由于Cl先前并不存在,所以是相当于新建了一个Cl类型信息体的工厂实例,同时,通过RegistClass传入类名称。接下来是AddVars以及AddFunc,这两个方法是类类型信息体工厂中的方法,通过这两种方法创建各成员域的实例并存入类类型信息体中。这样就完成了动态反射中一个类的注册过程。

结语

进行到这里,不知道各位有没有发现一个问题?虽然我们实现了注册,但貌似并没有说明如何通过动态反射注册的信息来调用实例的方法。是的,和静态反射有些许不同,动态类型还需要一个模块才能实现完全调用:Any模块,不过这一部分我打算放在下篇博文中讲述。毕竟这一次的博文知识密度也不低。希望大家可以消化充分。好的,下期见!

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行过许可