【GAD翻译馆】C 反射机制:元数据类型(二)类型推导和成员

发表于2017-10-17
评论0 3.3k浏览

翻译:王成林(麦克斯韦的麦斯威尔 ) 审校:黄秀美(厚德载物)

上一篇文章中我们学习了建立反射系统的基本知识。核心思想是用户可以使用一个位于cpp文件中的宏DEFINE_META将类型手动添加到系统中。

在这篇文章中我要谈谈类型推导(type deduction)和成员反射,它们是实现其他功能的基础。

首先是类型推导。当使用模板化的元创建器(MetaCreator)类时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
template <typename metatype="">
 
class MetaCreator
 
{
 
public:
 
  MetaCreator( std::string name, unsigned size )
 
  {
 
    Init( name, size );
 
  }
 
  
 
  static void Init( std::string name, unsigned size )
 
  {
 
    Get( )->Init( name, size );
 
  }
 
  // Ensure a single instance can exist for this class type
 
  static MetaData *Get( void )
 
  {
 
    static MetaData instance;
 
    return &instance;
 
  }
 
};

每当你输入一个const,引用,或者指针类型的修饰符时,编译器会构建一个全新的模板化的元创建器。这样是不行的,因为我们不希望const int的元数据和int的元数据,或者其它任何注册过的类型的元数据有区别。有一个简单但是古怪的方法可以解决我们所有的问题。看看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//
// RemQual
// Strips down qualified types/references/pointers to a single unqualified type, for passing into
// a templated type as a typename parameter.
//
  
template <typename t="">
struct RemQual
{
  typedef T type;
};
  
template <typename t="">
struct RemQual<const t="">
{
  typedef T type;
};
  
template <typename t="">
struct RemQual<t&>
{
  typedef T type;
};
  
template <typename t="">
struct RemQual<const t&="">
{
  typedef T type;
};
  
template <typename t="">
struct RemQual<t&&>
{
  typedef T type;
};
  
template <typename t="">
struct RemQual<t *="">
{
  typedef T type;
};
  
template <typename t="">
struct RemQual<const t="" *="">
{
  typedef T type;
};

我找不到准确的术语来描述这里发生的事情,但是我会努力尝试。第一个RemQual结构体作为“标准”,有很多模板化的重载。标准结构体只有一个单一类型T,没有任何的限定符,没有任何指针或引用类型。其余的模板化重载版本都包含一个typedef,通过将重载的类型传递给结构体的类名(typename)参数,该结构体可以被用来引用一个未限定的类型。

我们还必须加入右值引用的重载从而还原到单纯的类型T

既然有了RemQualremove qualifiers,移除限定符)结构体,我们可以在META宏中使用它来指代元数据类型。这里我修改了三个META宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//
  // META_TYPE
  // Purpose: Retrieves the proper MetaData instance of an object by type.
  //
#define META_TYPE( TYPE ) (MetaCreator<remqual<type>::type>::Get( ))
  
  //
  // META
  // Purpose: Retrieves the proper MetaData instance of an object by an object's type.
  //
#define META( OBJECT ) (MetaCreator<remqual<decltype( object="" )="">::type>::Get( ))
  
  //
  // META_STR
  // Purpose : Finds a MetaData instance by string name
  //
#define META_STR( STRING ) (MetaManager::Get( STRING ))
  
  //
  // DEFINE_META
  // Purpose : Defines a MetaCreator for a specific type of data
  //
#define DEFINE_META( TYPE ) \
  MetaCreator<remqual<type>::type> NAME_GENERATOR( )( #TYPE, sizeof( TYPE ) )

思路是将RemQual中已经由typedef定义过的类型输入给元创建器的类名参数。这是这些宏的一个使用范例,你根本不可能用错它们,而且还能轻松地对它们进行故障排除,因为不存在任何滥用情况。你可以忽略那些你用不上的META宏。我使用了全部这三个宏META_TYPE,METAMETA_STR。你可以根据个人喜好实现它们,不过最好将生成的API放入到它们自己的命名空间中。

以上就是类型推导的全部内容了。其他一些方法也能实现同样的效果,比如这篇文章中介绍的局部模板特化(partial template specialization),虽然我觉得我的方法更简单。

接下来要介绍的是使用元数据系统注册结构体或者类的成员。在这之前,我们先看这个Member结构体例子。这个Member结构体是一个可以储存任何成员信息的容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//
// Member
// Purpose: Stores information (name and offset of member) about a data member of a specific class. Multiple
//          Member objects can be stored in MetaData objects within a std::vector.
//
class Member
{
public:
  Member( std::string string, unsigned val, MetaData *meta );
  ~Member( );
  
  const std::string &Name( void ) const; // Gettor for name
  unsigned Offset( void ) const; // Gettor for offset
  const MetaData *Meta( void ) const; // Gettor for data
  
private:
  std::string name;
  unsigned offset;
  const MetaData *data;
};

以上成员的实现和我自己反射系统中的几乎一致,并不需要很多内容。你仅需要一个描述所含数据类型的元数据实例,一个名称标识符,以及一个表示成员在所含对象中位置的无符号偏移值。该偏移值对于自动序列化非常重要,也许我会在后续文章中详细解释。

我们的思路是元数据实例可以包含多个成员对象。这些成员对象被保存在某种容器中(比如std::vector)。

为了添加一个成员我们需要另一个非常简单的宏。在这里使用宏效率会很高,主要原因有两个:我们可以使用字符串化算符#;用户在使用时不会把它们搞混。

在展示宏之前我想要说明如何得到偏移值。非常简单。使用数字0,将其转换为一个指向某种对象类型(类或者结构体)的指针。在类型转换后,使用->算符读取其中一个成员。最后,使用&算符得到该成员位置(即->算符得到的从0开始的偏移量)的地址,然后将其类型转换为一个无符号的整数。这是代码:

1
2
#define ADD_MEMBER( MEMBER ) \
  AddMember( #MEMBER, (unsigned)(&(NullCast( )->MEMBER)), META( NullCast( )->MEMBER ))

这行代码简单粗暴!它还很好地展示了宏的优势所在;它有一个输入参数,并将其应用在了不同的地方。使用这个宏用户会很难出错。

NullCast是一个函数,我在这一段后面会说明。它的作用是返回一个指向NULL(内存地址为0)的某种类型的指针。有了这个指向地址0的指针,我们可以使用ADD_MEMBER宏提供要读取的成员名称。然后该成员会被读取,而&算符会为该成员提供一个包含偏移值的地址。接着该值会被类型转换为一个无符号的整数,然后被传递给宏之中的AddMember函数。字符串化算符#也被用来向AddMember函数传递一个字符串表达式以及一个该成员类型的元数据实例。

那么我们应该在哪里调用这个AddMember函数?它应该出现在哪里?实际上它应该位于函数定义中。AddMember函数本身位于元创建器中。这使得元创建器可以调用它保存的元数据实例的AddMember函数,然后该函数会将成员对象添加到元数据实例中的成员容器中。

基于上一篇教程的内容,唯一一个可以调用该函数的地方是在元创建器的构造函数中。我们可以使用DEFINE_META宏创建一个元创建器的构造函数的定义,或者创建一个从元创建器的构造函数中调用的元创建器方法。这是例子:

1
2
3
4
5
6
DEFINE_META( GameObject )
{
  ADD_MEMBER( ID );
  ADD_MEMBER( active );
  ADD_MEMBER( components );
}

如你所见,该构建过程非常直观;它拥有C 式句法,你能清楚地了解这里发生了什么。一个GameObject在元数据系统中进行注册,它的IDactivecomponent这三个成员被添加到元数据系统中。为了让你更好地理解,这里是GameObject实际的类定义可能的样子(假设它是基于组件的结构):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class GameObject
{
public:
  // Sends all components within this object the same message
  void SendMessage( Message *msg );
  bool HasComponent( std::string& name ) const;
  void AddComponent( std::string componentType );
  Component *GetComponent( std::string name ) const;
  handle GetID( void ) const;
  
  handle ID;
  
// This boolean should always be true when the object is alive. If this is
  // set to false, then the ObjectFactory will clean it up and delete this object
  // during its inactive sweep when the ObjectFactory's update is called.
  bool active;
  std::vector components;
private:
  // Only to be used by the factory!
  GameObject( ) : ID( -1 ), active( true ) {}
  ~GameObject( );
};

现在我们看看新的DEFINE_META宏可能的样子:

1
2
3
#define DEFINE_META( TYPE ) \
  MetaCreator<remqual<type>::type> NAME_GENERATOR( )( #TYPE, sizeof( TYPE ) ); \
  void MetaCreator<remqual<type>::type>::RegisterMetaData( void )

RegisterMetaData声明显得非常奇特,因为宏定义在这里结束了。该宏的作用是建立起RegisterMetaData函数的定义,这样的话我们就可以将ADD_MEMBER宏的调用放在定义中了。RegisterMetaData函数应该从元创建器的构造函数中被调用。这使得用户可以轻松且直观地指定要反射某类型的元数据实例的哪个成员。

最后一点也很重要,我们快速说说NullCast函数。它位于元创建器中,因为NullCast需要模板的类名MetaType以返回一个指向某一个特定类型数据的指针。

就是这样了!现在我们可以保存类成员的信息,并且可以轻松地按照自己的方式推导某对象的类型。

这是一个示范项目的链接,该项目是使用Visual Studio 2010编译的。我确定稍作修改就可以在GCC中编译,但是我不想示范了,因为我太困了!这是该程序的输出结果。格式是<类型><大小>。如果是对象的话,会输出其成员和它们的偏移值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int
4
  
float
4
  
std::string
28
  
Object
8
{
  ID
  0
  active
  4
}

你也许注意到了你不能反射私有数据成员!这个细节我会在后续文章中讲到。大体思路是你要获取你想要反射的类源代码的使用权限,然后在其中加入一小段代码来得到私有数据读取权限。可以这样做,也可以将元创建器设为它的友元类(我觉得这个方法很麻烦)。

以上就是自动序列化所需的全部基础内容了!我们可以反射一个对象的成员的名称,它们的类型以及偏移值,这样使得反射系统可以注册任何类型的C 数据。


【版权声明】

原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

标签: