Question regarding vtables and lookups in code versions

139 Views Asked by At

I am working with a collaborative code base on a physics experiment. The general use case, say for particles, goes something like this:

// 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; }
}

Then the experiment collects huge amounts of data on particles which are stored in a database (useing CERN's ROOT TTrees, which is another story altogether) and the client code generally has something like:

   Particle* part = ttree->get_particle();
   std::cout << part->get_momentum() << std::endl;

where the ttree object is returning instances of some inherited class of Particle.

If the code is updated to Code_Version_v2 with a new method added to the base class Particle, but then uses Particle_v1 from the prior code version, can it cause somevtable method call errors?

For example:

// 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; }
}

And then the code does something like:

// 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

Does the c++ standard guarantee that the compilers etc... will be smart enough to properly implement the vtable lookups or not? I realize this may be dependent on the ROOT implementation in how it saves and streams objects from the TTrees. I have some subtle bug which disappears with the addition of cout statements, so it appears to be some kind fo memory corruption. All insights are appreciated. Thanks!

1

There are 1 best solutions below

0
On

If the code is updated to Code_Version_v2 with a new method added to the base class Particle, but then uses Particle_v1 from the prior code version, can it cause somevtable method call errors?

Per the standard one is not allowed to have two different definitions of an entity in the same program at all. Different here means that no single token in the definition may be different (plus even stricter requirements). That's part of the one-definition rule (ODR).

Adding a function to a class clearly violates that and therefore, generally it is required that you recompile every library/translation unit that includes the class definition in order to get any guarantee that the program will behave correctly.

The standard makes no guarantees on the program or compiler behavior if ODR is violated like that.


In practice requirements are a bit weaker. Compilers use a specified ABI for interfacing between binary code of multiple libraries or translation units. If one knows how this ABI is specified (and there are many relatively obvious commonalities given how compilers compile C++ to machine code), then one can get away with some modifications without recompiling libraries.

For example, adding a non-virtual non-special non-constructor/desctructor member function, that wouldn't have been chosen anywhere by overload resolution in the old code, is (in ABIs that I am aware of) not a problem, because it doesn't affect object layout, naming mangling or calling conventions, etc.

However, adding a virtual member function, even a non-special one, does affect layout of the vtable and generally is not ABI compatible. If the class previously didn't have a virtual member function it also affects the memory layout of the class itself.

If may be possible to make this change smoothly, assuming you know exactly how the vtable is laid out per ABI and compiler. E.g. it may be possible that because of the ordering of the member functions, the pointer for get_mass is know to be appended at the end of the vtable, so that order and use of previous entries isn't affected. And if it is then known that only Code_Version_v2 emits the vtable for Particle_v2, knowing about adding the additional entry, then it might be ok. But all of that is relying on good understanding of the particular ABI/compiler implementation, not any standard guarantees.

It will also probably go wrong again as soon as some other class derived from Particle adds new virtual member functions, because then the same slot in the vtable will probably be considered to refer to different functions by different libraries. Again, to be sure one would have to read through the ABI specification of the vtable layouts and probably make some assumptions about the rest of the program's code that must be adhered to. In either way, it is not safe for unrestricted use of the class.