记C++坑:4.虚函数表项的初始化时机

来源:互联网 发布:剑三团队插件数据导入 编辑:程序博客网 时间:2024/05/22 17:49

最近在设计一族工具类,用于将程序中所有的io部分进行整合封装,首选方案是采用c++的多态,将所有的io操作抽象成三大块:IoObject(pipe,tcp,ssl socket, http, file,...),IoPackageWrapper(用于数据序列化和反序列化),TransModule(主要是使用一些异步io模型,如socket的select,poll等),三大块分别抽象出基类,并根据实际的IO类型(如socket还是http)继承出不同的实现类,并且根据需要提供对应的创建工厂。

本来整个设计完成的很顺利,demo测试也通过了,然后就是代码结构的优化。

在AbstractTransModule的代码中有一处实现感觉略微粗糙,精简代码如下:

class AbstractTransModule{public:virtual ~AbstractTransModule() = 0{m_running = false;if(m_loopThread){m_loopThread.join();delete m_loopThread;m_loopThread = nullptr;}}void run(){assert(!m_running && !m_loopThread);m_loopThread = new std::thread(std::bind(&AbstractTransModule::_LoopFunc,this));}protected:virtual void _LoopFunc() = 0;bool m_running = false;std::thread* m_loopThread = nullptr;};

然后觉得这个run函数略显鸡肋,本着把接口精简到最少,让使用者用起来最爽的初心,开始优化代码,干掉run函数,依照RAII思想,将线程创建放到构造函数中去,改造后的代码如下:

class AbstractTransModule{public:AbstractTransModule(){m_loopThread = new std::thread(std::bind(&AbstractTransModule::_LoopFunc, this));}virtual ~AbstractTransModule() = 0{m_running = false;if(m_loopThread){m_loopThread.join();delete m_loopThread;m_loopThread = nullptr;}}protected:virtual void _LoopFunc() = 0;bool m_running = true;std::thread* m_loopThread = nullptr;};
感觉没啥问题,既然前一种写法已经测试通过了,后面这个编译完没毛病就打算提交了,正想着问题就这么被轻而易举而且优雅的解决了的时候,几次demo跑下来,总会偶尔出现一两次程序异常,并且没有堆栈可以参考。

还好自己联想推断和排查能力还不错,两种用法比较来看的话,主要区别就是线程创建的时机,而改动后变成了在构造函数里面,而且是基类的构造函数,因为具有继承关系的父子类之间,是先构造基类,再构造子类,对象是像滚雪球一样一层一层丰富起来的,而且_LoopFunc是一个虚函数,用其指针初始化一个bind生成的对象没有问题,但是一旦线程执行起来,调用这个函数,那么如果这个虚函数所在的虚函数表项还没有被初始化,应该就会出错。找这么一想,觉得问题很有可能就出在这里。

但是std::thread设置了回调函数之后就没办法控制它的调用时机了,所以是不是说就没有办法将县城创建从run函数中移到构造函数而去掉鸡肋的run了呢,当然是有方法的。并且可以顺便求证一下我的猜想,新代码如下:

class AbstractTransModule{public:AbstractTransModule(){m_loopThread = new std::thread(std::bind(&AbstractTransModule::_LoopFuncCaller, this));}virtual ~AbstractTransModule() = 0{m_running = false;if(m_loopThread){m_loopThread.join();delete m_loopThread;m_loopThread = nullptr;}}protected:void _LoopFuncCaller() { _LoopFunc(); }virtual void _LoopFunc() = 0;bool m_running = true;std::thread* m_loopThread = nullptr;};
对_LoopFunc加了一层包装,那么在线程执行起来之后_LoopFuncCaller被调用,这一步肯定是不会出错的,好,接下来我用新的代码来测试demo,果然还是异常,跟踪堆栈,发现定位就在:
void _LoopFuncCaller() { _LoopFunc(); }

这一句。然后断点调试,果然如我猜想,正是线程跑起来的时候子类部分还没有构造完成,虚函数表项中的指针还没有指向正确的函数,所以,只需要在_LoopFuncCaller中调用_LoopFunc之前等待虚函数表初始化完成就可以了。

想起来挺容易,但是该怎么做呢,根据以往的经验加上搜集资料验证,类对象的虚函数表指针就是this指针,所以,这个事情就变得不那么棘手了,虽然C++没有提供方法让我们访问虚函数表,但是鉴于c++基于c语言基础建立起来的多态解决方案,自己实现一个检查虚函数表初始化完成与否的工具也是很容易的。,我的一个简单实现:

template<typename T>class VfTableInitChecker{public:~VfTableInitChecker(){if (m_unInitVftalbe){delete[] m_unInitVftalbe;m_unInitVftalbe = nullptr;}}private:void RecordUnInitVfTable(const T* _this)//在基类的构造函数总调用{if (!std::is_polymorphic_v<std::remove_reference_t<T>>)return;void** p = *(void***)this, **pt = p;int count = 0;while (*(pt++)) { ++count; }m_unInitVftalbe = new void*[count] {};memcpy(m_unInitVftalbe, p, sizeof(void*)*count);}bool IsVfTableInitDone()//在异步函数中调用以检查完成{if (!std::is_polymorphic_v<std::remove_reference_t<T>>)return true;void** p = *(void***)this;int i = 0;while ((*p) && (*p != m_unInitVftalbe[i++])) { ++p; }return !(*p);}void WaitVfTableInitDone()//在异步函数中调用以等待完成{while (!IsVfTableInitDone())std::this_thread::sleep_for(std::chrono::milliseconds(1));}private:void** m_unInitVftalbe = nullptr;};

然后AbstractTransMoudle就可以写成这样:
class AbstractTransModule{public:AbstractTransModule(){m_vftchecker.RecordUnInitVfTable(this);m_loopThread = new std::thread(std::bind(&AbstractTransModule::_LoopFuncCaller, this));}virtual ~AbstractTransModule() = 0{m_running = false;if(m_loopThread){m_loopThread.join();delete m_loopThread;m_loopThread = nullptr;}}protected:void _LoopFuncCaller(){m_vftchecker.WaitVfTableInitDone();_LoopFunc();}virtual void _LoopFunc() = 0;bool m_running = true;std::thread* m_loopThread = nullptr;VfTableInitChecker<AbstractTransModule> m_vftchecker;};

好了,至此,一个小小的RAII的代码优化总算结束了。

或许你会问,大费周章就完成这个一个小的改动有什么意义,并且里面看起来还带了点对虚函数表的奇技淫巧。归根结底一句话,就是为了让对上层暴露的接口更加清晰简洁明了,不然的话,要么向一开始那样,提供一个run,对用户而言就会显得突兀,还有一种方案就是在每一个AbstractTransMoudle子类构造完成之后自己去加启动线程的代码,那么一层继承还好,多层继承呢?这是其一,其二,本来的设计思路就是用户重写只需要实现_LoopFunc的实际io处理部分,但是实际情况是他还需要考虑我在什么时候去调用自己写的_LoopFunc函数。


最后,再追记一点笔记,c++虚函数表说起来是一个大家都知道的东西,并且也都知道几个概念:编译期确定,运行期赋值,偶尔面试时候也会被问到相关问题,基本都能答出来,画个内存结构也都没有问题,但是毕竟在实际项目中遇到关于虚函数表处理引起的问题还是比较少的,所以大部分的知识也都知识浮于概念罢了,就比如文中提到的所谓的“坑”。仔细想来也都在这些概念中总结过。

原创粉丝点击