C++ 对象析构事件
虚函数表修改和析构事件处理。
引言
本教程尝试描述如何通过修改对象的虚函数表(vtable)条目来订阅函数调用事件,并特别演示通过修改 vtable 来挂钩对象析构函数的代码示例。 通常,这在 C++ 编码中可能不是必需的,但对于读者来说,这是关于 vtable 和虚函数的额外信息。
背景
需求产生于我们拥有超过 100,000 个对象(例如 Book)存储在一个容器(例如 Library)中,并且 Library 一直被工作线程修改。 容器中的 Books 又在 GUI 控件中以活动 Book 的形式可视化。 问题出现在活动 Book 被工作线程删除,而 GUI 线程尝试访问相同对象时。
评估了几种解决方案,v-table 重载是其中之一。互联网上有很多关于构建 v-table 的讨论,但这些看起来可能不够通用,因为编译器可以自由选择 v-table 的格式。 因此,决定使用编译器本身来构建我们的自定义 v-table。
这种方法中唯一的假设是 v-table 指针存储在对象内存布局的前 4 个字节中,如果这个假设不正确,那么示例代码将无法工作。
自定义 v-table
实现目标更复杂的部分是构建 v-table,但 v-table 的内存布局可能与编译器相关。 因此,我们考虑使用编译器智能来构建我们的自定义 v-table,方法是从 Book 类继承 book_New 类(没有任何状态信息,如成员变量或访问基类成员变量),并在必要时重载虚函数。 现在,book_New 的 v-table 将被挂钩到 book 对象的实例上。
使用代码
示例代码被实现为一个模板,用于挂钩对象析构(这是我们的主要目的)。 示例代码将仅支持挂钩一个对象和一个观察者,但该模板应被修改为支持多个对象并挂钩其他成员函数。
让我们看一下 Book 类
class Book {
protected:
char* name_;
public:
Book(char* pstr) {
name_=_strdup(pstr);
};
Book(){};
void Display() {printf("Book Name:%s\n",name_);};
virtual ~Book() {
printf("virtual BOOK::~BOOK for :%s\n",name_);
delete name_ ;
};
};
这是一个简单的 C++ 类,用于记住书名并具有虚析构函数,保留虚析构函数的原因是我们需要挂钩析构事件。
析构函数 Hook
接下来要看的一段代码是 DestructorHook 模板类
IDestructorObserver 接口
class IDestructorObserver {
public:
// invoked when hooked object is destroyed
virtual void OnDestroy(T* pObj) = 0;
};
这是一个简单的 C++ 接口,它有一个用于析构的事件回调,并将有问题的对象作为参数传递。 容器应该实现这个接口,以便获得感兴趣对象的析构通知。
现在让我们检查模板类的成员变量
T* data_; // Generic pointer to be stored
IDestructorObserver* lifeTimeObserver_;
void* vtable_;
data_ 指的是感兴趣的对象,这里这个变量最终存储 Book 对象,lifeTimeObserver_ 用于将事件分发给感兴趣的各方,最后 vtable_ 用于存储 Book 的 vtable 指针,以便在取消挂钩时恢复。 这些数据成员应保存在 vector 或 map 类型的数据结构中,以支持多个对象和多个观察者。
实例方法将简单地初始化 lifeTimeObserver_ 成员并返回 Hook 对象。 返回的 hook 对象应用于挂钩 T 类型的对象
static DestructorHook<T>& Instance(IDestructorObserver* proc) {
staticObj.lifeTimeObserver_ =proc;
staticObj.name_=_strdup("Librarian Hook");
return staticObj;
};
Hook 方法将备份被挂钩对象的 vtable 并将其保留以供将来参考,然后用我们的自定义 vtable 覆盖,如下所示
void Hook(T* objPtr) {
UnHook();
// copy first four bytes in MSVC this points to __vfptr
memcpy(&vtable_,objPtr,sizeof(void*));
// __vfptr of objPtr with ours
memcpy(objPtr,this,sizeof(void*));
data_ = objPtr;
}
析构函数,当被挂钩对象被删除时,将调用此函数,在过滤掉自身析构后,事件将被分派给订阅者
virtual ~DestructorHook() {
// object is getting destroyed
// so we have to unhook anyway
staticObj.unHook();
if(this == staticObj.data_) {
// destructor invoked in the context of hooked object
// so dispatch event
staticObj.lifeTimeObserver_->OnDestroy(staticObj.data_);
// no need to call the destructor as the destruction cycle
// continues to call base destructor
staticObj.data_ = 0;
}
}
最后是主函数,它初始化 Books 对象,Librarian 对象将订阅析构事件。 稍后,Book 对象被销毁,librarian 将收到被挂钩对象的通知
int _tmain(int argc, _TCHAR* argv[]) {
// initialize chip
Book * pBook[5];
char name[20] = {0};
for(int i = 0 ; i < 5; i++) {
sprintf_s(name,"book%d",(i+1));
pBook[i] = new Book(name);
}
Librarian librarian;
BookDestructorHook& hook_ = BookDestructorHook::Instance(static_cast<BookDestructorHook::IDestructorObserver*>(&librarian));
hook_.Hook(pBook[3]);
printf("Display available books\n");
for(int i = 0 ; i < 5; i++) {
pBook[i]->Display();
}
printf("\n\nDeleting all Book Librarian should get notification for\t");
pBook[3]->Display();
for(int i = 0 ; i < 5; i++) {
delete pBook[i];
}
return 0;
}
输出:

结论
本文展示了一个非常简单的示例,说明如何修改 C++ vtable 条目来处理对象析构事件。 虽然我们没有使用这种方法,因为我们更喜欢引用计数机制,但个人认为这种替代方法应在时间紧迫的应用程序中考虑,在这种应用程序中,会持续创建和销毁数万本 Book,但容器只对单个对象感兴趣。 通过采用这种方法,剩余的对象不应承担引用计数实现的负担。