Search code examples
c++gccbinary-compatibility

adding virtual function to the end of the class declaration avoids binary incompatibility?


Could someone explain to me why adding a virtual function to the end of a class declaration avoids binary incompatibility?

If I have:

class A
{ 
  public:
    virtual ~A();
    virtual void someFuncA() = 0;
    virtual void someFuncB() = 0;
    virtual void other1() = 0;
  private:
    int someVal;
};

And later modify this class declaration to:

class A
{ 
  public:
    virtual ~A();
    virtual void someFuncA() = 0;
    virtual void someFuncB() = 0;
    virtual void someFuncC() = 0;
    virtual void other1() = 0;
  private:
    int someVal;
};

I get a coredump from another .so compiled against the previous declaration. But if I put someFuncC() at the end of the class declaration (after "int someVal"):

class A
{ 
  public:
    virtual ~A();
    virtual void someFuncA() = 0;
    virtual void someFuncB() = 0;
    virtual void other1() = 0;
  private:
    int someVal;
  public:
    virtual void someFuncC() = 0;
};

I don't see coredump anymore. Could someone tell me why this is? And does this trick always work?

PS. compiler is gcc, does this work with other compilers?


Solution

  • I'm a bit surprised that this particular rearrangement helps at all. It's certainly not guaranteed to work.

    The class you give above will normally be translated to something on this order:

    typedef void (*vfunc)(void);
    
    struct __A__impl { 
         vfunc __vtable_ptr;
         int someVal;
    };
    
    __A__impl__init(__A__impl *object) { 
        static vfunc virtual_functions[] = { __A__dtor, __A__someFuncA, __A__someFuncB};
        object->__vtable__ptr = virtual_functions;    
    }
    

    When/if you add someFuncC, you should normally get another entry added to the class' virtual functions table. If the compiler arranges that before any of the other functions, you'll run into a problem where attempting to invoke one function actually invokes another. As long as its address is at the end of the virtual function table, things should still work. C++ doesn't guarantee anything about how vtables are arranged though (or even that there is a vtable).

    With respect to normal data, (non-static) members are required to be arranged in ascending order as long as there isn't an intervening access specificer (public:, protected: or private:).

    If the compiler followed the same rules when mapping virtual function declarations to vtable positions, your first attempt should work, but your second could break. Obviously enough, there's no guarantee of that though -- as long as it works consistently, the compiler can arrange the vtable about any way it wants to.