USRefl源码分析

最近在看UE4中的反射机制,学了一些反射的使用方式,但并不知道其实现方式。这两天上知乎看到了Ubp.a大神写的99行静态反射,所以拿来分析一下源码。

其知乎文章在这里

一些小工具

这里先来看一些小工具,以方便后面接触反射核心代码。

TStr

首先是TStr,这是个编译期字符串,其实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template<typename C, C... chars>
struct TStr {
    using Char = C;

    template<typename T>
    static constexpr bool Is(T = {}) { return std::is_same_v<T, TStr>; }

    static constexpr const Char* Data() { return data; }
    static constexpr std::size_t Size() { return sizeof...(chars); }
    static constexpr std::basic_string_view<Char> View() { return data; }

private:
    static constexpr Char data[]{ chars...,Char(0) };
};

创建一个字符串的方法是这样:

1
TStr<char, 'h', 'e', 'l', 'l', 'o', 'w'> str;

这里的模板参数C是指字符的类型,chars...则是字符串中所有的字符。

首先看data[]的定义,他就是一个字符数组,并且在末尾增加了0,这里是将模板中的字符串存储了下来。

接下来看Is(T)函数,这个函数判断两个TStr是否相等。注意这里虽然字符串存到了data[]中但不能使用strcmp比较,因为strcmp只能运行在运行期。这里使用std::is_same_v进行判断。 这是因为对于两个不同的字符串,他们的类型是不一样的:

1
2
TStr<char, 'h', 'e', 'l', 'l'> str1; -> 类型为 TStr<char, 'h', 'e', 'l', 'l'>
TStr<char, 'o', 'w', 't', 'r'> str2; -> 类型为 TStr<char, 'o', 'w', 't', 'r'>

接下来要看一下两个创建TStr的函数,但是在此之前要介绍一下std::index_sequence

std::index_sequenceinteger_sequence<size_t>的模板别名,他存储着编译期的一个序列。std::make_index_sequence则可以生成他。

1
2
template<std::size_t... Ints>
using index_sequence = std::integer_sequence<std::size_t, Ints...>;

注意,这个类型主要是为了用在模板参数中,它并不像数组一样,他只有一个成员函数size(),这意味着你不能通过seq[0]这种方式得到他的值。

要想得的值只能使用一些模板技术(例子来自cppreference):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
template<typename T, T... ints>
void print_sequence(std::integer_sequence<T, ints...> int_seq)
{
    std::cout << "The sequence of size " << int_seq.size() << ": ";
    ((std::cout << ints << ' '),...);   // 这里是Fold Expression
    std::cout << '\n';
}

int main() {
    print_sequence(std::integer_sequence<unsigned, 9, 2, 5, 1, 9, 1, 6>{});
}

现在回头来这两个函数:

1
2
3
4
5
6
7
8
9
template<typename Char, typename T, std::size_t... Ns>
constexpr auto TSTRHI(std::index_sequence<Ns...>) {
    return TStr<Char, T::get()[Ns]...>{};
}

template<typename T>
constexpr auto TSTRH(T){
    return TSTRHI<typename decltype(T::get())::value_type,T>(std::make_index_sequence<T::get().size()>{});
}

T::get()[NS]...是指从T类型中调用静态函数get(),这个get会返回一个可索引的对象,然后将此对象的内容在这里展开:

1
2
3
4
TSTRHI(std::index_sequence<1, 2, 3>());

// 展开成
return TStr<Char, T::get()[1], T::get()[2], T::get()[3]>

其他的编译期函数

1
2
3
4
5
6
7
template<class L, class F>
constexpr std::size_t FindIf(const L&, F&&, std::index_sequence<>) { return -1; }

template<class L, class F, std::size_t N0, std::size_t... Ns>
constexpr std::size_t FindIf(const L& l, F&& f, std::index_sequence<N0, Ns...>) {
    return f(l.template Get<N0>()) ? N0 : FindIf(l, std::forward<F>(f), std::index_sequence<Ns...>{});
}

这两个函数是经典的递归式模板函数,用于在一个index_sequence中找到特定的数。其中f是谓词函数,用于对数字进行条件判断。

其他的函数也大同小异(只是功能不同),就不细说了。

核心的反射实现

Field的实现

Field是保存类中成员的结构,是反射的核心,它是存储着类中成员变量和函数的容器:

1
2
3
4
5
template<class Name, class T, class AList>
struct Field : FTraits<T>, NamedValue<Name, T> {
    AList attrs;
    constexpr Field(Name, T v, AList as = {}) : NamedValue<Name, T>{ v }, attrs{ as } {}
};

可以看出他存储了NamedValueattrs。其中attrsAList类型,这是用于给类型附加用户自定义信息的,属于这个反射系统中可有可无的东西,我们不管他。

先看继承的第一个类FTraits

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template<bool s, bool f>
struct FTraitsB { static constexpr bool is_static = s, is_func = f; };

// [1]
template<class T>
struct FTraits : FTraitsB<true, false> {}; // default is enum

// [2]
template<class U, class T>
struct FTraits<T U::*> : FTraitsB<false, std::is_function_v<T>> {};

// [3]
template<class T>
struct FTraits<T*> : FTraitsB<true, std::is_function_v<T>>{}; // static member

FTraits类型系列是用于自动判断类型T(和U)是类中的哪种成员。s代表是否是类中静态成员,f代表是否是函数。

[1]处的是默认值,即默认是类的静态变量。

[2]处的T U::*是指向类成员指针的表示形式(不清楚的看我的这篇文章),所以置s为false,但到底是不是函数还得用std::is_function_v判断一下。

[3]处的T*一定是静态成员,所以置s为true,但可能是指向变量的指针或者函数指针,所以要额外对f进行判断。

然后再看NamedValue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template<class Name, class T>
struct NamedValue : NamedValueBase<Name> {
    T value;
    static constexpr bool has_value = true;
    constexpr NamedValue(T v) : value{ v } {}

    template<class U>
    constexpr bool operator==(U v) const {
        if constexpr (std::is_same_v<T, U>)
            return value == v;
        else
            return false;
    }
};

template<class Name>
struct NamedValue<Name, void> : NamedValueBase<Name> {
    static constexpr bool has_value = false;

    template<class U>
    constexpr bool operator==(U) const {
        return false;
    }
};

NamedValue从广义上来说就是存储了个有名字的变量value(名字在其父类NamedValueBasename成员中)。在这里它是存储着成员字段。

然后看一下他的父类NamedValueBase,其实他的父类很简单,就是存了一个std::string_view,即成员的名称:

1
2
3
4
5
template<class Name>
struct NamedValueBase {
    using TName = Name;
    static constexpr std::string_view name = TName::View(); 
};

最后再通过Field的用法来彻底弄清楚这个玩意的用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct Vec {
  float x;
  float y;
  float norm() const { return std::sqrt(x*x + y*y); }
};

template<>
struct Ubpa::USRefl::TypeInfo<Vec> :
  TypeInfoBase<Vec>
{
  static constexpr FieldList fields = {
    Field {TSTR("x")   , &Type::x   },
    Field {TSTR("y")   , &Type::y   },
    Field {TSTR("norm"), &Type::norm},
  };
};

这里的Field的Name模板参数是TSTR("x")是个TStr,T则是float Vec::*也就是我们说的指向类成员的指针。那么这个时候NamedValue类就存储了成员的名字(TSRT("x")中的data成员,只不过是通过std::string_view的方式得到的,存在其父类NamedValueBase的name中)和指向成员的指针(NamedValue中的value)。

然后FTraits父类此时也存储了能够判断此成员是否静态,是否是函数的信息。所以总结下来,Field就是存储了指向类成员指针和名字的类,并且还能判断指向的是静态的还是非静态的,是函数还是变量。

上面代码中有一个FieldList,它是ElemList的子类,存储着所有的Field并且提供了查找,增加功能。

ElemList

ElemList是作者自己造的一个能够存储任意类型的列表,其内部使用了tuple实现。简单来说就是个tuple的封装,但比起tuple提供了更多的操作(如查找,询问是否包含,对每个元素进行操作和增加元素等)。操作主要是利用经典的递归模板技术,我们这里主要着眼于使用tuple存储各个类型的技巧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<typename...Es>
struct ElemList {
    std::tuple<Es...> elems;

    static constexpr std::size_t size = sizeof...(Es);

    constexpr ElemList(Es... elems) : elems{ elems... } {}

    //...
};

tuple可谓是实现反射中的核心和大哥大,因为它能够存储不同类型的变量。这里ElemList就是将Es类型存放到了tuple中。

TypeInfoBase

TypeInfoBase主要是存储着父类的信息。其存储的类型都是Base类的子类:

1
2
3
4
template<class T, bool IsVirtual = false> struct Base {
    static constexpr auto info = TypeInfo<T>{};
    static constexpr bool is_virtual = IsVirtual;
};

可以通过这个类得到类的信息,以及是否是虚类。

我们简单地看看TypeInfoBase

1
2
3
4
5
6
7
8
template<class T, typename... Bases>
struct TypeInfoBase {
    using Type = T;

    static constexpr BaseList bases{ Bases{}... };

    // ...
};

他将所有的基类全部存储到BaseList,这是ElemList的子类:

1
2
3
4
template<typename...Bs>
struct BaseList : ElemList<Bs...> {
    constexpr BaseList(Bs... bs) : ElemList<Bs...>{ bs... } {}
};

这里的Base是你使用此库时手动加上去的:

1
2
3
4
5
6
7
8
9
template<>
struct Ubpa::USRefl::TypeInfo<C> :
	TypeInfoBase<C, Base<A>>    // <--- 这里
{
	static constexpr AttrList attrs = {};
	static constexpr FieldList fields = {
		Field {TSTR("c"), &Type::c},
	};
};

而且你得先对A类进行反射(也Base类要知道A类的反射信息)。

其他的TypeInfoBase成员函数就是普通的查找,增加等操作了(都是ElemList中的操作),没什么可说的。

总结

UML如下

UML

其中TypeInfo是一个空类,用户需要全特化他并且将自己类型的信息放入(使用Field)。

其实说难也不难,都是模板的常见操作,但是合起来就觉得挺麻烦的了。

updatedupdated2023-06-082023-06-08