我正在使用物理实验的协作代码库。一般用例,比如粒子,是这样的:
// Code_Version_v1:
class Particle {
public:
virtual float get_momentum() { return NAN; }
}
class Particle_v1 : public Particle {
private:
momentum;
public:
override float get_momentum() { return momentum; }
}
然后实验收集了大量的粒子数据,这些数据存储在数据库中(使用CERN的ROOT TTrees,这完全是另一个故事),客户端代码通常有这样的内容:
Particle* part = ttree->get_particle();
std::cout << part->get_momentum() << std::endl;
其中 ttree 对象返回某个 Particle 继承类的实例。
如果代码更新到Code_Version_v2,并在基类Particle中添加了新方法,但随后使用之前代码版本中的Particle_v1,是否会导致某些vtable方法调用错误?
例如:
// Code_Version_v2 library
class Particle { // update with a new method
public:
virtual float get_momentum() { return NAN; }
virtual float get_mass() { return NAN; }
}
// class Particle_v1 is unchanged
class Particle_v2 : public Particle {
private:
float momentum;
float mass;
public:
override float get_momentum() { return momentum; }
override float get_mass() { return mass; }
}
然后代码会执行以下操作:
// Using Code_Version_v2 library
Particle* part = ttree->get_particle(0); // returns Particle_v1* generated with Code_Version_v1
std::cout << part->get_momentum() << std::endl; // now run with libraries from Code_Version_v2
C++ 标准是否保证编译器等...将足够智能以正确实现 vtable 查找?我意识到这可能取决于 ROOT 实现如何保存和流式传输 TTree 中的对象。我有一些微妙的错误,随着添加
cout
语句而消失,所以它似乎是某种内存损坏。所有见解均受到赞赏。谢谢!
是的,当基类和派生类跨库版本扩展时,特别是在涉及 vtable 时,C++ 中可能会出现 ABI 兼容性问题。在基类中添加新的虚拟方法将重新布局 vtable,可能会破坏二进制兼容性。
在您的示例中,如果使用
Particle_v1
创建了 Code_Version_v1
对象,并且稍后链接到 Code_Version_v2
,则调用 get_mass()
将是未定义的行为,因为 vtable 布局已更改,但 Particle_v1
对象仍遵循旧布局。
使用添加的
cout
语句进行调试可以修改内存布局或访问时序,从而可能掩盖问题(“Heisenbug”)。
最佳实践:
ROOT 的 TTree 序列化可能会使情况进一步复杂化,特别是如果它不考虑 ABI 更改的话。因此,在升级与多态对象的序列化形式互操作的库时应格外小心。