views:

184

answers:

5

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?

+1  A: 

Maybe G++ puts its "private" virtual table in a different place than its public one.

At any rate, if you can somehow trick the compiler into thinking an object looks differently than it really does and then use that object you are going to invoke nasal demons. You can't depend on anything they do. This is basically what you are doing.

Noah Roberts
+5  A: 

From another answer:

Whether this leads to a memory leak, wipes your hard disk, gets you pregnant, makes nasty Nasal Demons chasing you around your apartment, or lets everything work fine with no apparent problems, is undefined. It might be this way with one compiler, and change with another, change with a new compiler version, with each new compilation, with the moon phases, your mood, or depending on the number or neutrinos that passed through the processor on the last sunny afternoon. Or it might not.

All that, and an infinite amount of other possibilities are put into one term: Undefined behavior:

Just stay away from it.

If you know how a particular compiler version implements its features, you might get this to work. Or you might not. Or you might think it works, but it breaks, but only if whoever sits in front of the computer just ate a yogurt.

Is there any reason why you want this to work anyway?

I suppose it works/doesn't work the way it does/doesn't because your compiler creates virtual table entries in the order the virtual functions are declared. If you mess up the order by putting a virtual function in between the others, then when someone calls other1(), instead someFuncC() is called, possibly with wrong arguments, but definitely at the wrong moment. (Be glad it crashes immediately.)
This is, however, just a guess, and even if it is a right one, unless your gcc version comes with a document somewhere describing this, there's no guarantee it will work this way tomorrow even with the same compiler version.

sbi
+1 Coincidentally, I am eating yogurt right now, so should I try and see if g++ lets me get away with some nasty undefined stuff? :)
FredOverflow
@FredOverflow: Then just add to the manual that users are required to eat yogurt while using your application and a good deal with a big dairy might make you more money than the application itself. `:-o`
sbi
@sbi Hm, wouldn't it be sufficient if I eat yogurt during compilation? Is undefined behavior associated with compile-time, runtime or both? Or is that also undefined? :)
FredOverflow
@FredOverflow: I believe it's the behavior of the generated executable that's undefined, but I think a compiler might still be standard-conforming if it rejected code that leads to undefined behavior. At least with some cases of UB. Or would it? `<sigh>` I'm the exact opposite of a lawyer...
sbi
@sbi It would be nice if compilers could warn more about undefined behavior. `g++ --nose-exorcist` or something like that.
FredOverflow
+1  A: 

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.

Jerry Coffin
updated problem description. I assume adding to the end of the class declaration works because gcc will add the new virtual function to the end of the vTable?
bob
@bob: as long as its declaration follows the others, I'd kind of expect that anyway. Noah Roberts may be right though -- maybe it's somehow related to making it private, so perhaps it doesn't get put into the normal vtable at all (it would be sort of unnecessary there, since polymorphic invocation doesn't make much sense for a function that can only exist in one class).
Jerry Coffin
A: 

I suggest avoiding anything related to binary compatibility or serialization with a class. This opens up too many cans of worms and there is not enough fish to eat them all.

When transferring data, prefer XML or an ASCII based protocol with field definitions over a binary protocol. A flexible protocal will help people (including you) debug and maintain the protocols. ASCII and XML protocols offer a higher readability than binary.

Classes, objects, structs and the like, should never be compared as a whole by binary image. The preferred method is for each class to implement its own comparison operators or functions. After all, the class is the expert on how to compare its data members. Some data members, such a pointers and containers, can really screw up binary comparisons.

Since processors are fast, memory is cheap, change your principles from space saving to correctness and robustness. Optimize for speed or space after the program is bug-free.

There are too many horror stories posted to Stack Overflow and the Newsgroups related to binary protocols and binary comparisons (and assignments) of objects. Let's reduce the amount of new horror stories by learning from the existing ones.

Thomas Matthews
The question isn't about serializing data, it's about shared library (in)compatibility (i.e. a fragile-base-class type of problem)
Jeremy Friesner
A: 

Binary compatibility is a bad thing. You should use a plaintext-based system, perhaps XML.

Edit: Misplaced the meaning of your question a little. Windows provides many native ways to share data, for example GetProcAddress, __declspec(dllexport) and __declspec(dllimport). GCC must offer something similar. Binary serialization is a bad thing.

Edit again: Actually, he didn't mention executables in his post. At all. Nor what he was trying to use his binary compatibility for.

DeadMG
The question is about C++ executable code compatibility, not about data serialization compatibility.
Jeremy Friesner
I'd like to be pointed at a dynamic library linking system using plain-text to link executables. _Please._
sbi
Ah. I missed that. Thought he was discussing binary serialization, say to file. Edited my answer.
DeadMG