Godot Source Code Note 3
非静态成员函数指针类型擦除
在Godot源码中,Object类中声明并定义了一个名为_bind_methods的函数:
该函数通过调用ClassDB类中的静态方法bind_method()完成Object类中成员函数的注册。
由于Godot中关于Scene的类均直接或间接继承自Object类,因此类似Node3D、Area3D的类就可以覆写(注意:不是虚函数的重写)父类中bind_method()方法完成自身类中成员函数的注册。
class_db.h文件中对ClassDB::bind_method()函数的定义:
args数组存储函数的默认实参,argptrs数组则存储args数组中对应函数默认实参的地址。接下来通过create_method_bind函数,将成员函数指针包装成一个MethodBind派生类对象,并返回MethodBind基类指针。
create_method_bind函数的四个重载版本定义在method_bind.h文件中:
在此以第一种重载版本为例,其余不再赘述,其实现如下:
这里主要讨论无TYPED_METHOD_BIND宏的MethodBindT类构造方法(MethodBindT类继承自MethodBind类),这是简化了的MethodBindT类定义:
可以看出MethodBindT类有一个void (MB_T::*)(P...)类型的成员变量method,而其构造函数接受一个相同类型的变量p_method并将其赋值给method。
从create_method_bind函数中也可以看到,在MethodBindT构造函数中,reinterpret_cast<void (MB_T::*)(P...)>(p_method)将p_method重新解释为void (MB_T::*)(P...)类型的成员函数指针。
在代码中找到了关于MB_T的部分:
也就是说,MB_T只是__UnexistingClass的别名,但__UnexistingClass只有声明没有定义!
为什么要这么做?
为了弄清这个问题,不得不简要了解一下C++类中的内存布局。
概念上来说,从C++类中实例化出的每个对象,都独立拥有非静态的成员变量数据与成员函数代码。
单独为每个对象保存成员函数代码段会造成大量内存浪费,因此成员函数代码段其实是仅有一份且共享地址的。
不难猜测,&ClassName::FunctionName取得的结构体一定包含成员函数地址(注意:为什么是结构体,而不仅仅是函数地址,后面会提到)。
众所周知,非静态成员函数的调用必须要在对象中,那么仅仅知道成员函数地址还不行,因此编译器会在调用成员函数时传入this指针以代表当前对象。这样,就可以完成对象对成员函数的调用。
这个所谓的成员函数指针到底是什么?
首先根据之前的推测,成员函数指针中一定包含了对应函数代码段地址。
那么为什么成员函数指针无法转换为一个函数地址呢?
考虑一下多继承的情况,比如D类同时继承了A类和B类,那么在调用父类(A类和B类)成员函数时,需要根据父类(A类和B类)在子类(D类)中的内存布局来调整this指针并传入对应父类(A类和B类)成员函数中。
那么很容易想到,成员函数指针包含的另一个数据肯定与类中内存布局有关,即父类在子类中的偏移量,而这就可满足计算得到父类this指针的要求。
总结一下,成员函数指针应该是一个结构体,其中包含了成员函数代码段地址与类的内存偏移量。
&ClassName::FunctionName是对象无关的,因此不经过对象也能直接取到成员函数指针也就不奇怪了。
当然了,以上仅为一种可能的实现方式,具体实现根据平台不同存在差异。
解释清楚成员函数指针,reinterpret_cast<void (MB_T::*)(P...)>(p_method)也就很好理解了。
任意类型的成员函数指针p_method,可以直接解释为结构相同的__UnexistingClass::*类型,并赋值给method变量。
由此便完成了对成员函数指针的类型擦除。
从MethodBindT的构造函数中返回,就得到了成员函数指针的MethodBind包装类型的指针。
接下来将类的名称存储在MethodBind对象的成员变量中,将指针从create_method_bind中返回。
标记返回值类型是否为Object指针后,调用bind_methodfi()函数完成非静态成员函数注册。
最后再看一下成员函数指针的调用方法:
传入具体对象完成成员函数的调用,语法为:
最后再附一个简化后的例子:
但在C++11后可以使用std::function库函数完成对函数的包装:
如侵删。
欢迎评论指正。