找回密码
 立即注册
查看: 199|回复: 0

现代图形引擎入门指南(五)— 宏 模板 反射

[复制链接]
发表于 2023-1-23 07:36 | 显示全部楼层 |阅读模式
相信大多数小伙伴,在找到学习的窍门之后,继续深耕已经没什么困难了,在这之后的一个阶段的学习与尝试,其实都主要围绕着两个字  —— 偷懒
宏(Macro)

C++代码在参与编译的之前,有一个预编译的过程,该过程会使用预处理器来处理代码中的 预处理指令 ,不同的编译器有不用的预处理指令,比如Microsoft C/C++的预处理器可以识别以下指令:

  • #define:定义宏
  • #if:条件逻辑判断
  • #ifdef:if define 判断
  • #ifndef:if not define 判断
  • #elif:else if
  • #else
  • #endif:逻辑判断终止
  • #error:抛出错误
  • #include:包含头文件
  • #line:修正提供给编译器的行号
  • #pragma:Pragma 指令指定特定于机器或操作系统的编译器功能
  • #undef:取消宏的定义
  • ...
使用#define可以定义宏,使用#undef可以取消之前的宏定义,宏的逻辑可以看作是简单的字符替换,基本用法如下:
#define EMPTY_MACRO
#define SRC Dst

#define EMPTY_MACRO_WITH_PARAMS()
#define F(Param) Param
#define F_STR(Param) #Param
#define F_MERGE(Param0,Param1) Param0##_##Param1
#define F_VARIADIC(...) __VA_ARGS__

int main() {
    EMPTY_MACRO
    int SRC;
    EMPTY_MACRO_WITH_PARAMS()
    F(const char*) F_MERGE(Const, Text) = F_STR(F_VARIADIC(A, B, C, D, E));
    return 0;
}
上面的代码经预处理阶段之后,将变成:
int main() {
    int Dst;
    const char* Const_Text = "ABCDE";
    return 0;
}
一般情况下,C++程序中的宏定义来自于:

  • 编译器的预定义宏
  • 提供构建工具添加的宏
  • 代码中使用#define定义的宏
编译器的预定义宏,以MSVC为例,这里有一个详细的预定义宏列表:

  • https://learn.microsoft.com/en-us/cpp/preprocessor/predefined-macros?view=msvc-170
对于构建工具,例如cmake,提供了函数target_compile_definitions用于为构建目标添加宏定义:
target_compile_definitions(<target>
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])宏的用途,主要有以下:
编译分支


  • 可以使用宏作为开关来切换到不同的编译分支
/*跨平台编译分支*/
#ifdef _WIN32
   #ifdef _WIN64
      //define something for Windows (64-bit only)
   #else
      //define something for Windows (32-bit only)
   #endif
#elif __APPLE__
    #include "TargetConditionals.h"
    #if TARGET_IPHONE_SIMULATOR
         // iOS Simulator
    #elif TARGET_OS_IPHONE
        // iOS device
    #elif TARGET_OS_MAC
        // Other kinds of Mac OS
    #else
    #   error "Unknown Apple platform"
    #endif
#elif __ANDROID__
    // android
#elif __linux__
    // linux
#elif __unix__ // all unices not caught above
    // Unix
#elif defined(_POSIX_VERSION)
    // POSIX
#else
#   error "Unknown compiler"
#endif

/*版本编译分支*/
#if _MSC_VER >= 1910
// . . .
#elif _MSC_VER >= 1900
// . . .
#else
// . . .
#endif
代码简化


  • 可以将一些固定步骤的代码替换成宏从而简化代码
比如:
/*简化操作*/
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define MIN(a,b) ((a) < (b) ? (a) : (b))

/*定义常量*/
#define M_PI 3.1415926535

/*使用这一组宏来生成类的相关操作,例如将类注册到脚本中..*/
#define CLASS_BEGIN(ClassName) ...
#define CLASS_ADD_PROPERTY(PropertyName) ...
#define CLASS_ADD_FUNCTION(FunctionName) ...
#define CLASS_END() ...

/*批量处理*/
define FOR_EACH_NUMBER_TYPE(FuncBegin,Func)\
  FuncBegin(int) \
  Func(float) \
  Func(double) \
  Func(short) \
  Func(unsigned int)
#define NUMBER_PREPEND_COMMAN(Type) ,Type
#define NUMBER_BEGIN(Type) Type
// FOR_EACH_NUMBER_TYPE(NUMBER_BEGIN, NUMBER_PREPEND_COMMAN)
// 将展开为 int,float,double,unsigned int
还有一些更高级的用法,比如用宏实现递归来处理某些东西:

  • https://github.com/pfultz2/Cloak/wiki/C-Preprocessor-tricks,-tips,-and-idioms
调试提示


  • 一些预定义宏提供了很多上下文信息
#include <iostream>

int main(){
    std::cout << __DATE__<<"|"
    << __TIME__ << "|"
    << __FILE__ << "|"
    << __LINE__ << "|"
    << __FUNCTION__ << std::endl;
    return 0;
}
上述代码将打印:
Jan 15 2023|21:49:58|C:\Users\Administrator\source\repos\Macro\main.cpp|4|main
缺陷

宏并不是万能的,它也伴随一些严重的问题:

  • 功能简单,仅仅只是字符串替换,对于参数,只有 拼接转字符串 的操作
  • 宏会给程序增加很多非C++标准之外的魔幻语法,过度使用会给开发者增加不少认知负担,使维护变得困难
  • 宏展开的代码,无法使用编译器调试
模板(Template)

大家常见的模板示例应该是 C++ 标准库中的各类容器和算法,诸如 std::vector、std::map、std::sort()...
根据笔者目前的阅历来看,它主要被大量使用在一些跟类型强相关的工具库中,比如:容器、算法、序列化、反射、脚本绑定...
学习目标

对于游戏开发人员而言,并不要求对模板有过多的深入,但可能需要了解特化、偏特化、类型萃取的概念,并掌握以下技能:

  • 通过模板来做一些判断:比如判断某个类型是否符合某种条件,某种结构是否存在...
class Base {
};

class Derived : public Base{
public:
void Test() {}
};

template< typename T>
struct has_test_function
{
typedef char Yes;
typedef struct { char d[2]; } No;

template<typename Proxy>
static Yes test(decltype(&Proxy::Test));
template<typename Proxy>
static No test(...);

static const bool value = (sizeof(test<T>(0)) == sizeof(Yes));
};

int main() {
//判断是否是浮点类型
std::cout << std::is_floating_point<int>::value << std::endl;  //0
std::cout << std::is_floating_point<float>::value << std::endl; //1
std::cout << std::is_floating_point<double>::value/span> <span class="o"><< std::endl;//1

//判断类的继承关系
std::cout << std::is_base_of<Base, Derived>::value << std::endl;//1

//判断类中是否存在Test函数
std::cout << has_test_function<Base>::value << std::endl;  //0
std::cout << has_test_function<Derived>::value << std::endl; //1
return 0;
}
在标准库中,输入 std::is ,IDE会弹出很多可供使用的模板函数


  • 通过模板特化、偏特化来控制结构分支:很多模板工具库都会通过模板的特化、偏特化来提供一些扩展点,下面是一个不错的示例:
    假如有这样的需求:
    有一个元素是 序列容器 的std::vector,希望通过序列容器的元素尺寸进行排序,就比如std::vector<std::vector<int>>,它的元素类型std::vector<int>就是一个序列容器,最终我们想得到这样的效果:
std::vector<std::vector<int>> vec = {
    {0,1,2},
    {0,1},
    {0,1,2,3,4},
    {0,1,2,3},
    {0}
};

/* 排序之后应该如下 */
std::vector<std::vector<int>> vec = {
    {0},
    {0,1},
    {0,1,2},
    {0,1,2,3},
    {0,1,2,3,4}
};
我们需要关注的点是:


    • 如何确定类型是否是序列容器?
    • 因为要实现std::sort的排序机制,就需要考虑如何 允许 且 只允许序列容器 通过这个机制来进行排序:

最终的代码如下:
#include <iostream>
#include <algorithm>
#include <vector>
#include <list>

template<typename _Ty>
struct sequential_container {     //用于判断一个类型是否是序列容器以及得到序列容器的尺寸,默认为false 和 0
static_assert(false,"Invalid Type")   //使用非序列容器报错
static int size(const _Ty& containter){ return 0;}
static const bool isVaild = false;
};


template<typename _Ty>
struct sequential_container<std::vector<_Ty>>{ //通过偏特化,指定std::vector为序列容器,并实现它的size函数
static int size(const std::vector<_Ty>& containter) { return containter.size(); }
static const bool isVaild = true;
};

//通过std::enable_if限定范围
template<typename _ItemType, typename std::enable_if<sequential_container<_ItemType>::isVaild>::type* = nullptr>
void sequential_container_sort(std::vector<_ItemType>& vec) {
std::sort(vec.begin(),vec.end(),[](const _ItemType& Lhs, const _ItemType& Rhs){
  return sequential_container<_ItemType>::size(Lhs) < sequential_container<_ItemType>::size(Rhs);
});
}

int main() {
std::vector<std::vector<int>> vec = {
  {0,1,2},
  {0,1},
  {0,1,2,3,4},
  {0,1,2,3},
  {0}
};
sequential_container_sort(vec);
return 0;
}
如果后续要扩展其他序列容器,只需通过模板特化或偏特化:
template<>     //扩展std::string
struct sequential_container<std::string> {  
static int size(const std::string& containter) { return containter.size(); }
static const bool isVaild = true;
};

template<typename _Ty>  //扩展std::list
struct sequential_container<std::list<_Ty>> {
static int size(const std::list<_Ty>& containter) { return containter.size(); }
static const bool isVaild = true;
};
学习方式

对于想要深入学习模板的小伙伴,大家可以到Github上寻找一些模板使用比较的多的仓库进行学习,这里罗列一下笔者学习过的几个库:

  • Rttr:使用模板实现的反射库
  • Sol2:C++绑定到Lua的便捷库
  • Bitsery:二进制序列化库
  • EASTL:追求在游戏中高效的容器和算法库
  • UnLua:Unreal引擎到Lua的绑定库
这里有一本不错的书籍:


还有一个不错的教程

  • https://github.com/wuye9036/CppTemplateTutorial
对于一个逻辑开发人员而言,模板并不是必备项
反射(Reflection)

对于 ,它可以在预处理阶段进行代码替换
对于 模板 ,它能使C++中的类、结构、函数能够随类型变化
它们在C++中都有着明确的语法,但反射不同,它不属于C++以及编译器标准,它更像是一种机制 —— 将代码中的枚举、类、结构、函数...作为运行时可访问甚至操作的资产它本质上是C++代码的自省
对于大部分开发者而言,无需深追反射的实现原理,仅需了解反射的主体结构和使用规范即可。
这其中能完成的操作包括但不限于:

  • 根据名称 读写 对象的属性
  • 根据名称 调用 函数
  • 根据类名称创建实例
  • 根据名称判断类型间继承关系
  • 迭代对象的 所有属性、方法和枚举
  • 不同类型间的隐式适配
  • 为类型,属性,函数,参数追加元数据
当下而言,相对比较完善的反射库有:

  • UHT (Unreal Header Tool):用于Unreal Engine
  • MOC(Meta Object  Compiler):用于Qt
  • RTTR(Run Time Type Reflection):开源反射库
这些反射库对上述操作均有支持,每个实现都带有一定"特色",比如:

  • Unreal Engine 通过 UHT 的反射实现了编辑器的自动绑定和生成,对象的自动序列化,蓝图脚本,引用分析,垃圾回收,网络同步...
  • Qt 通过 Moc 支持了可视化UI编辑,QML脚本
以上面的操作条目1为例,作为C++的使用者,在你不知道什么是反射的情况下,要根据属性名称对其进行读写,你可能会写出下面的代码:
class Example{
public:
    void setProperty(std::string name, int var){
        if(name == "a")
            a = var;
        else if(name == "b")
            b = var;
    }
private:
    int a;
    int b;
};
上面代码虽然简单,但是它确实可以满足需求,或许我们还能做一些优化:

  • if else 过于缓慢,我们可以通过构建映射来加速:
class Example{
public:
    void setProperty(std::string name, int var) {
        *PropertyMap.at(name) = var;
    }
private:
    int a = 0;
    int b = 0;
    std::unordered_map<std::string, int* > PropertyMap = {
        {"a",&a},
        {"b",&b}
    };
};
上面我们为每个Example实例记录了它的变量地址,但每个Example对象都构造一个PropertyMap似乎有些浪费,我们是否可以改为Example类只有一个Property Map?
很显然是可以的,由于Example的内存结构是确定的,我们只需要使用记录变量在内存中的偏移 Offset, 最后 this 的地址 +Offset 即可得到变量的地址。
class Example {
public:
    void setProperty(std::string name, int var) {
        int offset = PropertyMap[name];
        int* valuePtr = (int*)((char*)this + offset);  //注意指针+的跨度是一个元素的长度,所以这里先将this转char*,+offset即是 + offset个字节
        *valuePtr = var;
    }
private:
    int a = 0;
    int b = 0;
    static std::unordered_map<std::string, int> PropertyMap;;
};

std::unordered_map<std::string, int> Example::PropertyMap  {
    {"a", offsetof(Example, a)},
    {"b", offsetof(Example, b) }
};
上面的代码从结构上来看几乎无可挑剔,但是却很鸡肋——setValue只能设置int类型的变量。那是否能做到不同类型都能通过同一个函数设置呢?大神们第一时间想到的可能是模板,他们或许会写出这样的代码:
    template<typename _Ty>
    void setProperty(std::string name, _Ty var) {
        int offset = PropertyMap[name];
        _Ty* valuePtr = (_Ty*)((char*)this + offset);  //注意指针+的跨度是一个元素的长度,所以这里先将this转char*,+offset即是 + offset个字节
        memcpy(valuePtr, &var, sizeof(_Ty));
    }
现在的代码从功能上来说,已经很完美了,但是它还有一个问题,要求在调用setProperty的时候必须明确属性的类型,虽然我们可以通过偏特化来做类型的验证,但大量使用模板将会导致代码的急剧膨胀,所以我们迫切需要一种可供验证的类型擦除手段。
对于这个问题,大家应该很容易想到解决方案 ——只需要提供一个 void* 记录数据地址,一个TypeID记录类型即可
在反射框架中,一般称这个结构为 Variant
但它并非一个void*加一个TypeID擦除了类型就完事了,还需要注意:

  • Variant的存储的数据类型是多样的,类型的擦除和还原,数据的拷贝,构造,析构,往往是会差异,根据差异,反射框架一般会将这些类型划分为:

    • 基础类型(int、double、char...)
    • Class/Struct
    • 指针
    • 容器(序列,散列)

因此 Variant 往往还带有一个 Flag 用来标识类型的特征,为了能够让Property支持Variant的处理,所以还需要存储属性的类型ID,为了更直观一些,我们使用这样的结构:
struct MetaProperty{
    variant read(void* ObjectPtr){
        return variant::readFromProperty(typeId,ObjectPtr,offset);
    }
   
    void write(void* ObjectPtr,variant var){
        if(var.canConvert(typeID)){
            var.writeToProperty(ObjectPtr,offset);
        }
    }
   
    std::string name;
    int offset;
    int typeID;
}

struct MetaClass{
    std::unordered_map<std::string, MetaProperty> properties;
}
在反射框架中,一般会使用如下结构存储反射信息


  • MetaClass:类的所有信息

    • MetaFunction:描述函数的信息,函数的参数,ID(或地址)...
    • MetaProperty:描述属性的信息,属性的类型,地址偏移...
    • MetaEnum:描述枚举的信息

  • MetaType:为每一个类型提供唯一的MetaType,用于在运行时进行类型的逻辑处理
  • Variant:类型变体,用于擦除C++类型,与MetaType强相关
伪代码如下:
class Example {
public:
    void setProperty(std::string name, variant var) {
        StaticMetaClass.Properties[name]->write(this,var);
    }
private:
    int a = 0;
    int b = 0;
   
    friend class MetaClass;
    static MetaClass StaticMetaClass;
};

MetaClass Example::staticMetaClass = {
    {"a",{"a",variant::GetType<int>(),offsetof(Example, a)}},
    {"b",{"b",variant::GetType<int>(),offsetof(Example, b)}},
}
对于条目2【根据函数名称调用函数】,这里列一个简单的核心结构:
#include <iostream>
#include <string>

class Example {
public:
    void print(int a) {                 //函数样例1
        std::cout << a << std::endl;
    }
    double add(double a, double b) {     //函数样例2
        return a + b;
    }

    template<typename _TyParam0>
    bool invoke(std::string name, const _TyParam0& param0) {        //适配只有单个参数的函数
        void* params[2] = { nullptr,(void*)&param0 };
        return invoke_internal(name, params);
    }

    template<typename _TyRet, typename _TyParam0,typename _TyParam1>
    bool invoke(std::string name, _TyRet& ret, const _TyParam0& param0, const  _TyParam1& param1) {     //适配带有两个参数且有返回值的函数
        void* params[3] = { (void*)&ret,(void*)&param0,(void*)&param1 };
        return invoke_internal(name, params);
    }
private:
    bool invoke_internal(std::string name, void** params) {         //核心:根据参数堆栈来调用对应的函数,index 0 存返回值的指针
        if (name == "print") {
            print((*reinterpret_cast<int(*)>(params[1])));
            return true;
        }
        else if (name == "add") {
            double ret = add((*reinterpret_cast<double(*)>(params[1])), (*reinterpret_cast<double(*)>(params[2])));
            if (params[0]) {
                *reinterpret_cast<double*>(params[<span class="mi">0]) = std::move(ret);
                return true;
            }
        }
        return false;
    }
};

int main() {
    Example ex;
    ex.invoke("print", 5);
    double result;
    ex.invoke("add", result, 10.0, 5.0);
    std::cout << result << std::endl;
    return 0;
}
反射调用的核心是通过一个转接函数 invoke_internal 来根据函数名选择相应的函数,再从 void** params  中读取参数并还原调用,最后通过模板封装一层来适配不同的参数数量
当然,上述的结构也可存储到 MetaClassMetaFunction 中,除此之外,还有枚举( MetaEnum ),由于比较简单,笔者这里不做赘述。
综上,我们实现了两个小功能:

  • 根据名称 读写 对象的属性
  • 根据名称 调用 函数
在这两个小功能中,已经不知不觉的实现了反射,上面的实现,使得我们可以将变量 a、b,函数print、add,作为了程序运行时可访问甚至操作的资产。
你可能注意到了,为了让一个Class支持反射,我们需要实现很多固定结构的硬编码部分,比如:  MetaPropertyMetaFunctionMetaEnum的信息构造,invoke_internal 的编写,为了简化这个部分,正常情况下我们偷懒的方法无非就两种:

  • 宏:使用宏可以完成固定格式的代码生成

    • 缺点:它最大的痛点就在于它只是做简单的文本替换,所以在使用它做反射时功能非常受限。

  • 模板:模板元是近年来C++最狂战酷炫的编程范式,使用它可以做很多编译期的计算、逻辑分支。相较于宏,它具备足够的编程性和完整的C++环境。其中大名鼎鼎的反射框架(RTTR) ,就是通过模板生成的。

    • 缺点:

      • 模板的使用门槛较高
      • 模板的特性会带来一些问题,比如模板不能继承,需要放置到头文件,才能传递反射的绑定。
      • 最大的缺点还是需要手写一些绑定函数


尽管结合了上面的两种方法,反射的实现依旧具有一定的局限性,那还有其他办法吗?答案肯定是有的
你可能不敢想象这群丧心病狂的挂壁为了解决这么一点点的局限性,居然打起了C++编译器的主意。
它们的目的也很简单:就是写一个 自动写代码 的程序 (Code Generator)
说简单点,就是我要做一个程序,能够像模板那样,得到所有的代码信息,但不受限于模板语法,像宏那样,可以进行代码修改,但不仅仅是字符替换,总而言之,就是我要 根据代码信息随心所欲地生成代码
所以我们需要:

  • Header Parser:解析代码中定义的信息(一般是头文件)。
  • Code Generator:根据已有信息生成附加代码。
采用这种做的有Qt (Moc)和Unreal (UHT),它们的流程基本相似:

  • 约定标记:这里的标记指宏,使用标记的主要目的是为了让代码扫描工具快速搜集周围的有效信息,标记宏的用法主要有三种:

    • 不带参数的“空宏”:只起到标记的作用

      • 举例:Qt里的Q_INVOKABLE

    • 带参数的"空宏":除了标记之外,还可以向扫描工具中传递参数,从而生成个性化代码

      • 举例:UE里的UProperty(...)UFunction(..)等,Qt里的Q_PROPERTY(...)

    • 入口宏:附带一部分的定义

      • 举例:UE里的GENERATED_BODY(),它的定义是由UHT生成在gen.h中,Qt里的Q_OBJECT是固定填充一部分定义,示例如下:


#define Q_OBJECT \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
...

  • 代码解析&信息搜集

    • 这一过程主要由Header Parser完成(UE UHeaderTool  | Qt MOC),解析其实只是在扫描关键字并还原类的层次结构,并不涉及到语法相关的内容,QtMOC的Parser轻量且高效,能轻松解析函数,枚举,类型,类,而UE针对其工程提供了许多扩展。
    • 样例:
      假如约定了下面的标记:

AxPROPERTY(GET getX SET setX)
int x =0;
其解析过程看上去就是这样的:
void Moc::parser(){
//...
case AX_PROPERTY_TOKEN: //这段代码会在扫描到 AxPROPERTY 时触发
parseAxProperty(&def);
break;
//...
}

void Moc::parseAxProperty(ClassDef *def)
{
PropertyDef axVarDef;         //属性定义
next(LPAREN);                 //判断下一个是不是左括号
while (test(IDENTIFIER)) {        //判断是不是标识符(非关键字)
QByteArray type = lexem();    //获取类型
next();                       //扫描下一个关键字
if (type == "GET") {      
axVarDef.getter = lexem();
     }
else if (type == "SET") {
axVarDef.setter = lexem();
     }
}
next(RPAREN);                 //判断下一个是不是右括号
axVarDef.type = parseType();  //解析类型
next(IDENTIFIER);             //判断下一个是不是标识符
axVarDef.name = lexem();      //存储函数名
until(SEMIC);                 //一直往后扫描,直到分号
def->propertyList << axVarDef;    //将该属性添加到类中
}

  • 搜集到足够的代码信息,将使用Code Generator来生成代码

    • 对于Qt而言,会生成moc_*.cpp,它里面存放了之前我们需要手写的代码,就比如property的各类信息,function的invoke_internal函数等
    • 对于UE而言,它会生成_.generated.h _.gen.cpp:相较于Qt,UE多生成了一个头文件,这个文件的主要目的是为了生成GENERATED_BODY的定义,通过这个方法,UE甚至能够自定义地修改类定义,而Qt就只能在已有的接口上扩展。
    • 样例
      假如现在要用Code Generator利用属性信息生成代码

for(auto& property:def.propertyList){
fprintf(out,"        .property(\"%s\"",property.name.constData());
if (property.getter.isEmpty()) {
fprintf(out, ",&%s::%s)\n", def.classname.constData(), property.name.constData());
continue;
         }
fprintf(out, ",&%s::%s", def.classname.constData(), property.getter.constData());
if (!property.setter.isEmpty()) {
fprintf(out, ",&%s::%s", def.classname.constData(), property.setter.constData());
         }
fprintf(out, ")\n");
     }
.property("x",&TestClass::getX,&TestClass::setX)


    • 上面的代码可能会生成如下的代码:

.property("x",&TestClass::getX,&TestClass::setX)

  • 上述的步骤只完成了代码的解析和生成,真正将UHT和MOC实装到项目上还得依靠构建工具

    • UE通过UBT去调用UHT
    • Qt通过QMake去调用moc
    • 此外,CMake作为现在主流的构建工具,它也提供了相应的指令来支持这些操作,就比如:

add_custom_command(         //自定义命令,并指定依赖,当${INPUT_FILE_PATH})变动时,调用${CMD},生成 ${OUTPUT_FILE}   
              OUTPUT ${OUTPUT_FILE}               
              COMMAND ${CMD}
              DEPENDS ${INPUT_FILE_PATH})   
set_property(TARGET ${PROJECT_TARGET} APPEND PROPERTY SOURCES ${OUTPUT_FILE}) //将生成的代码文件添加到target的sources中有了这种Header Parser + Code Generator的机制,使得我们可以做更高级别的反射功能(我们可以根据自己的需求魔改C++代码):

  • 编辑器的自动绑定
  • 自动序列化
  • 脚本的自动绑定
  • 引用分析、垃圾回收
  • 网络同步
对于这些功能的实现,有着太多的细节和难点,个人认为去深究它们的实现原理,并没有太多的意义。

  • 对于使用者来说,只需要了解官方所制定的使用方式,底层上,粗略了解它们的工作流程即可。
  • 对于有同样开发需求的人来说,Code Generator一般是跟框架的核心机制强关联的,所以它里面会有非常多的黑话,整体思路上可以借鉴,但在细节上没必要盲目追求一致。
  • 对于轻量级的反射需求,Rttr是一个非常不错的选择
笔者对UE,Qt,RTTR都有不少的使用,整体用下来的感受如下:

  • 反射信息的支持程度:UE>=RTTR>Qt,UE和RTTR的反射信息通过注册全部存储到了一起,所以可以在全局统一处理所有的MetaClass,MetaFunction,MetaEnum,而Qt就显得有些保守,反射信息存储到Class局部,甚至都不支持MetaData,而UE的反射,就比较夸张了,它甚至可以手动去构造一个MetaClass,用它来创建Object(蓝图的原理)
  • 反射的扩展能力:UE > Qt > RTTR,这点主要是因为UE和Qt有反射编译器的加持
笔者这里也写了一些对反射的测试项目:

  • XObject : 使用STD库模仿Qt的MOC,通过扫描XObject的标记代码,生成Rttr的注册代码以及序列化方法
  • QDetailWidget:在Qt Moc的基础上,模仿UE的DetailView,支持自定义属性编辑器,撤销重做,允许序列容器,散列容器,共享指针的属性编辑(重构中...)

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-6-23 10:21 , Processed in 0.093803 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表