更健壮的 C++ 对象生命期管理

以下的这个 C++ 技巧是前段时间一个同事介绍给我的,而他是从 fmod 中看来。当时听过后没怎么在意,主要是因为这两年对 C++ 的奇技淫巧兴趣不大了。今天跟另一同事讨论一些设计问题时,突然觉得似乎在某些地方还有点用途,就向人介绍了一番。讲完了后觉得其实还是有点意思,不妨写在 blog 上。

问题的由来是这样的:音频播放的模块中比较难处理的一个问题是,波形(wave sample)数据对象的生命期管理问题。因为你拿到一个对象后,很可能只对它做一个播放(play)的操作,然后就不会再理会它了。但是这个对象又不能立刻被释放掉。因为声卡还在处理这些数据呢。我们往往希望在声音停止后,自动销毁掉这个对象。

另一些时候,我们还需要对正在播放的声音做更细致的控制。尤其在实现 3d 音效,或是做类似多普勒效应的声音效果的时候。

C++ 中传统的方法是用智能指针的方式来管理声音对象,但这依赖语言本身的一些特性。fmod 提供了诸多语言的接口,它在为 C++ 提供接口的时候利用了一个更为巧妙的方法。

声音的类工厂并不需要生产出一个真正的对象指针,而是返回一个唯一 ID 。但是在语法层面上看,它却是一个对象指针。也就是说,如果你用调试器去看这个指针的值,他很有可能是 1,2,3,4 这样的数字。

如果这个类不提供任何虚方法,也没有直接可以访问的成员变量的话,对这个指针做任何成员函数调用其实都是合法的。因为 C++ 的普通成员函数调用仅仅只是把对象指针做为一个特殊的叫做 this 的参数传入函数而已。比如这样的代码是完全可以正常运行的:

class A { public: void test() { printf("%d",(int)this); } }; int main() { ((A*)1)->test(); return 0; }

那么库的实现只需要在每个成员函数的开始,利用一张 hash 表,把 this 表示的 id 转换成内部真正的对象指针即可。如果这个 id 对应的对象已经被销毁,则可以安全的退出函数调用。

这个技巧提高了库的健壮性,其代价是每次成员函数调用都需要多一次 hash 表查询操作。如果想优化一下性能的话,不妨 cache 住最近访问过的对象。