【GAD翻译馆】C 反射机制:元数据类型简介
翻译:王成林(麦克斯韦的麦斯威尔 ) 审校:黄秀美(厚德载物)
原文链接:http://cecilsunkure.blogspot.com/2012/09/c-reflection-class-metadata-introduction.html
想要提高工作效率,思考如何进一步使用你的工具至关重要。有一种反射工具会对你的工作有很大的帮助。我管这种反射系统叫“元(meta)”或者“元数据(MetaData)”,虽然它正确的学名应该是元数据类(Class MetaData)或者类型反射(Type Reflection)。所以当我说“元数据”时我其实指的是关于数据的数据,尤其是C 代码中关于数据类型的数据。
有一个元数据系统可以在运行时保存有关数据类型的信息,以供运行时使用。C 编译器在编译时会删去很多信息,其中有很多是非常有用的。而元数据系统保存了这些信息以免它们被删除。
那么该系统的用途有哪些呢?以下是我目前构建的内容:简单而功能强大的故障排除工具;自动序列化所有使用Meta关键词注册过的类型;将函数自动绑定到Lua;Variant和RefVariant类型类(即可以保存任何使用Meta注册过的数据类型的对象)。然而元数据系统的用途不只局限于此。有了元数据你几乎不需任何额外代码就能实现一个简单的对象工厂(object factory),或者你可以轻松地构建一个属性查看表格。我确信还有很多用途我这里没有提及。某种程度上来讲,像这样的系统可以在新的类或者数据类型被引入时为开发者生成工具和某些必要的功能。
在我们开始之前我希望读者们知道该系统的效率非常高,足可以在一个实时程序(例如一个游戏)中完美地运行。我个人对于底层效率和优化懂得不多,但是我认识一些程序员他们制作出了速度非常快的反射系统。所以在开始之前不要对C 反射系统抱有任何先入为主的偏见。
我从这篇文章中学习到了如何构建一个元数据系统。你如果感兴趣可以看看,不过如果你只是想学习我在这里教你的内容就没必要看了。这篇文章是我们DigiPen的一个学生研究员写的,他粗略介绍了元数据的入门知识,但是有很多细节需要读者自己钻研。我的这篇教程是建立在他的文章基础上的,因为他的文章结构适合入门教程。
我会尽量将我全部的学习心得写在这里,据我目前所知网上还没有任何关于构建元数据系统的参考资料。我能找到的相关度最高的资料来自于一本叫做《Game Programming Gems》的书,第五版第1.4章,不过它要求所有相关类都要继承自元数据类。如果你想要对无法访问源代码的类或者结构类型进行反射,它就帮不上你的忙了。此外它还不支持内置类型。
开始
首先我们了解一下元数据系统的整体结构是什么样的。我想最好画一张图概括一下这篇文章要讲的内容:
元数据系统的总体布局图
在上图中,元数据对象为关键对象,所有重要的操作都在它内部完成。元数据对象是非模板类,我们可以将它存储在一个数据结构体中。每一个在系统中注册的数据类型都有一个元数据对象,而某一数据类型的元数据对象代表该数据类型。它储存了一些信息,包括它是否为普通旧式数据(Plain Old Data, POD)类型,该类型的大小,它的成员和方法,以及它的名称。可以同时存储多个继承信息,不过我还没有加入这个功能,因为它并不十分实用而且正确建立非常困难。
元数据创建器(MetaCreator)是一个模板类,它管理单独元数据实例的创建。在创建实例后,元数据创建器会将它添加到元数据管理器中,后者通过某种映射方法包含它。
元数据管理器(MetaManager)是一个非模板类,包含所有创建的元数据实例,可以对它们进行搜索寻找特定类型。我使用一种从字符串到实例的映射,所以我可以使用字符串标识符进行搜索。我还在元数据管理器中加入了一些其它不太重要的功能函数。
客户端代码
在写代码之前,我想要展示一些客户端的范例代码来解释我为什么要花这么多时间来创建一个元数据系统。
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 48 49 50 51 52 53 54 55 56 57 | GameObject *obj = FACTORY->CreateObject( "SampleObject" ); // FACTORY->CreateObject code GameObject *ObjectFactory::CreateObject( const std:: string & fileName ) { SERIALIZER->OpenFile( fileName ); GameObject *obj = DESERIALIZE( GameObject ); SERIALIZER->CloseFile( ); IDObject( obj ); LUA_SYSTEM->PushGameObject( obj ); return obj; } // LUA_SYSTEM->PushGameObject code void LuaSystem::PushGameObject( GameObject *obj ) { lua_getglobal( L , "GameObjects" ); lua_pushinteger( L, obj->GetID( ) ); LuaReference *luaObj = (LuaReference *)lua_newuserdata( L, sizeof ( LuaReference ) ); luaL_setmetatable( L, META_TYPE( GameObject )->MetaTableName( ) ); luaObj->ID = obj->GetID( ); luaObj->meta = META_TYPE( GameObject ); // grab MetaData instance of this type lua_settable( L, 1 ); // Stack index 1[3] = 2 // Clear the stack lua_settop( L, 0 ); } |
如你所见,我加入了一个DESERIALIZE宏,它可以反序列化在反射系统中注册过的任何类型的数据。我的整个序列化文件(包括导入和导出部分)只有约400行的代码,另外我还实现了我自己定义的文件格式。我另外还加入一个LuaReference数据类型,它包含了一个句柄和一个元数据实例,并且允许通过句柄将任意类发送给Lua。有了元系统,我可以很轻松地写出泛型且高效的代码。
开始
我建议你首先反射一个类型的大小以及它的名称。这是一个非常简单的元数据类:
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 | // // MetaData // Purpose: Object for holding various info about any C type for the MetaData reflection system. // class MetaData { public : MetaData( std:: string string , unsigned val ) : name( string ), size ( val ) {} ~MetaData( ) const std:: string & Name( void ) const { return name; } unsigned Size( void ) const { return size; } private : std:: string name; unsigned size; } |
这个简单的类只存储了一种数据类型的大小和名称。下一步我们加入能够创建元数据实例的能力。这需要一个名为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; } }; |
你应该能够发现将一种类型(比如<int>)输入到元数据创建器中之后,Get函数就有一个元数据实例选项了,且该元数据实例和MetaCreator<int>类相关联了。任何类型都可以被指定为Metatype类型名。元数据创建器的构造函数初始化了元数据实例。这很重要。在这之后,你会拥有一个包含POD类型元数据的类的元数据。然而,由于初始化的顺序问题,构造某类的元数据实例所需的一些类型还没有被初始化(比如在Init函数中它就没有被调用)。但是如果你使用MetaManager<>::Get()函数,你可以得到一个指向一块内存位置的指针,该指针会在这个特定类型的元创建器构造完成后被初始化。我们要注意元创建器的构建发生在一个宏之中,所以类型注册不会产生错误(不过宏的内部的确很乱……)。
最后你需要一个地方保存所有元数据实例:元数据管理器!
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 | // // MetaManager // Purpose: Just a collection of some functions for management of all the // various MetaData objects. // class MetaManager { public : typedef std::map<std:: string , const = "" metadata= "" *= "" > MetaMap; // Insert a MetaData into the map of objects static void RegisterMeta( const MetaData *instance ); // Retrieve a MetaData instance by string name from the map of MetaData objects static const MetaData *Get( std:: string name ); // NULL if not found // Safe and easy singleton for map of MetaData objects static MetaMap& GetMap( void ) { // Define static map here, so no need for explicit definition static MetaMap map; return map; } }; |
在那里我们可以有效地保存所有元数据实例。Get函数最大的作用是通过字符串名称得到某一类型的元数据实例。现在我们已经建立好了三个主要部分,下面我们讨论在元数据系统中实际注册某一类型时用到的宏。
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 | // // META_TYPE // Purpose: Retrieves the proper MetaData instance of an object by type. // #define META_TYPE( TYPE ) (MetaCreator<type>::Get( )) // // META // Purpose: Retrieves the proper MetaData instance of an object by an object's type. // #define META( OBJECT ) (MetaCreator<decltype( object="" )="">::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<type> NAME_GENERATOR( )( #TYPE, sizeof ( TYPE ) )</type> </decltype(></type> |
到目前为止一切还好。使用DEFINE_META宏我们可以轻松地向元数据系统添加一个类型,只需使用DEFINE_META(类型);即可。decltype作为一个新加入的功能可能让你感到费解。它其实就是返回一个对象的类型。这样我们无需知道对象是什么类型,就可以得到一个它对应的元数据实例;我们可以使用它写出非常泛型的代码。
NAME_GENERATOR稍微有些复杂。元创建器的每个实例都需要在全局范围中构建——这是使Init函数被调用的唯一方法,该方法不需要将你的DEFINE_META宏放在某个代码范围中。如果元创建器的构造函数没有调用Init,那么想要使用DEFINE_META运行代码我们需要将它放在一个在主函数后面的某个范围内。这会使DEFINE_META宏用起来更复杂。如果你在全局范围中创建了元创建器,那么你可以在主函数执行之前先运行其构造函数代码,使DEFINE_META宏简单易用。
接下来我们要解决如何为我们的元创建器命名的问题。首先你可能想到的是仅仅将其命名为MetaCreator,然后将其设为静态。这样会将元创建器隐藏在文件范围中,使DEFINE_META宏可以在每个文件中使用一次以避免命名冲突。但是,如果在一个文件中你需要不止一个DEFINE_META呢?我想到的方法是使用token粘贴算符##。这是token粘贴算符的一个使用示范:
1 2 3 | DEFINE_META( TYPE ) \ MetaCreator<type> Creator##TYPE( )( #TYPE, sizeof ( TYPE ), false ) </type> |
这个方法的唯一问题在于类型名称中不能含有特殊字符或者空格,因为这样会导致token名称错误。最后一个方法是使用一种唯一名称生成方法。每一次使用宏生成唯一名称时我们可以使用两个宏:_LINE_和_FILE_,前提是该宏没有在同一文件中的同一行代码中被使用两次。_LINE_和_FILE_使用起来很麻烦,但是我觉得我正确地使用了它们,像这样:
1 2 3 4 5 | #define PASTE( _, __ ) _##__ #define NAME_GENERATOR_INTERNAL( _ ) PASTE( GENERATED_NAME, _ ) #define NAME_GENERATOR( ) NAME_GENERATOR_INTERNAL( __COUNTER__ ) |
你必须仔细地输入_COUNTER_以确保它们被预处理器解释为正确的数值。得到的token可能像这样子:GENERATED_NAME_29。这样我们在栈中的全局范围内可以完美地创建所有的元创建器。不使用像这样的一些技巧,你就要用到一个很讨厌的函数调用来注册元数据。
另外还有_FILE_和_LINE_宏,但是它们不像_COUNTER_那样满足了我们所有的需要。不过我认为_COUNTER_不是标准宏。
到目前为止我认为所有内容都很简单。有问题的话请在评论中问我!
请注意const,&,和*三种类型会产生不同的元数据实例。有一个方法可以在使用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 25 26 27 28 29 30 31 32 33 34 35 | DEFINE_META( int ); DEFINE_META( float ); DEFINE_META( double ); DEFINE_META( std:: string ); void main( void ) { std::cout << META_TYPE( int )->Name( ); // output: "int" std::cout << META_TYPE( float )->Size( ); // output: "4" std:: string word = "This is a word" ; std::cout << META( word )->Name( ); // output: "std::string" std::cout << META_STR( "double" )->Size( ); // output: "8" if (META( word ) != META_TYPE( int ) // address comparison { std::cout << "This object is not an int!" ; } } |
就是这样了!一个非常易于使用的DEFINE_META宏,它存储了所有类型(包括类和结构体)的名称和大小。
后续文章:
在后面的文章中我希望介绍Lua自动化绑定,Variant和Refvariant,自动序列化,工厂使用,通信,也许还有一些其他话题。这些话题都很庞大,所以请耐心等待,要讲完所有内容需要很长一段时间了。
该系列第二篇链接。
【版权声明】
原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。