In C++, does overriding an existing virtual function break ABI?
Asked Answered
M

5

13

My library has two classes, a base class and a derived class. In the current version of the library the base class has a virtual function foo(), and the derived class does not override it. In the next version I'd like the derived class to override it. Does this break ABI? I know that introducing a new virtual function usually does, but this seems like a special case. My intuition is that it should be changing an offset in the vtbl, without actually changing the table's size.

Obviously since the C++ standard doesn't mandate a particular ABI this question is somewhat platform specific, but in practice what breaks and maintains ABI is similar across most compilers. I'm interested in GCC's behavior, but the more compilers people can answer for the more useful this question will be ;)

Maiocco answered 21/4, 2011 at 15:13 Comment(2)
If you're doing an ABI why don't you use plain C for the interfaces?Neal
Because it's a C++ library meant to be used in idiomatic C++ ways. Do you think say, QT should be offering their API only in C? :PMaiocco
M
11

It might.

You're wrong regarding the offset. The offset in the vtable is determined already. What will happen is that the Derived class constructor will replace the function pointer at that offset with the Derived override (by switching the in-class v-pointer to a new v-table). So it is, normally, ABI compatible.

There might be an issue though, because of optimization, and especially the devirtualization of function calls.

Normally, when you call a virtual function, the compiler introduces a lookup in the vtable via the vpointer. However, if it can deduce (statically) what the exact type of the object is, it can also deduce the exact function to call and shave off the virtual lookup.

Example:

struct Base {
  virtual void foo();
  virtual void bar();
};

struct Derived: Base {
  virtual void foo();
};

int main(int argc, char* argv[]) {
  Derived d;
  d.foo(); // It is necessarily Derived::foo
  d.bar(); // It is necessarily Base::bar
}

And in this case... simply linking with your new library will not pick up Derived::bar.

Millicent answered 21/4, 2011 at 15:21 Comment(7)
That's why the virtual methods should always be private.Megaspore
@grumm143: This is not sufficient though. Inlining can still kick in.Millicent
How? Virtual methods could only be called though public, non-virtual interface, which doesn't depend on the exact type of the class. The virtual calls could be inlined only within the library itself, but this obviously isn't an issue.Megaspore
@grumm143: Ah, then you would mean that public methods definitions are not exposed (to prevent inlining).Millicent
"the Derived class constructor will replace the function pointer at that offset with the Derived override" the constructor does not mess with the vtable. In fact the vtable are constants (but could need fixing by the dynamic linker at load time). A vtable never dynamically modified.Dorso
@grumm143 "That's why the virtual methods should always be private." hug?Dorso
@curiousguy: right, poorly exprimed, what is changed is the v-pointer; I added a clarification.Millicent
A
7

This doesn't seem like something that could be particularly relied on in general - as you said C++ ABI is pretty tricky (even down to compiler options).

That said I think you could use g++ -fdump-class-hierarchy before and after you made the change to see if either the parent or child vtables change in structure. If they don't it's probably "fairly" safe to assume you didn't break ABI.

Afrikander answered 21/4, 2011 at 15:20 Comment(1)
It does, if the compiler was able to devirtualize a number of function calls :/Millicent
D
3

Yes, in some situations, adding a reimplementation of a virtual function will change the layout of the virtual function table. That is the case if you're reimplementing a virtual function from a base that isn't the first base class (multiple-inheritance):

// V1
struct A { virtual void f(); };
struct B { virtual void g(); };
struct C : A, B { virtual void h(); }; //does not reimplement f or g;

// V2
struct C : A, B {
    virtual void h();
    virtual void g();  //added reimplementation of g()
};

This changes the layout of C's vtable by adding an entry for g() (thanks to "Gof" for bringing this to my attention in the first place, as a comment in http://marcmutz.wordpress.com/2010/07/25/bcsc-gotcha-reimplementing-a-virtual-function/).

Also, as mentioned elsewhere, you get a problem if the class you're overriding the function in is used by users of your library in a way where the static type is equal to the dynamic type. This can be the case after you new'ed it:

MyClass * c = new MyClass;
c->myVirtualFunction(); // not actually virtual at runtime

or created it on the stack:

MyClass c;
c.myVirtualFunction(); // not actually virtual at runtime

The reason for this is an optimisation called "de-virtualisation". If the compiler can prove, at compile time, what the dynamic type of the object is, it will not emit the indirection through the virtual function table, but instead call the correct function directly.

Now, if users compiled against an old version of you library, the compiler will have inserted a call to the most-derived reimplementation of the virtual method. If, in a newer version of your library, you override this virtual function in a more-derived class, code compiled against the old library will still call the old function, whereas new code or code where the compiler could not prove the dynamic type of the object at compile time, will go through the virtual function table. So, a given instance of the class may be confronted, at runtime, with calls to the base class' function that it cannot intercept, potentially creating violations of class invariants.

Dedie answered 21/4, 2011 at 16:2 Comment(1)
Thanks for sharing this info. In my mind Mark's suggestion to use g++ -fdump-class-hierarchy would be the winner here, right after having proper regression tests ;)Gera
D
1

My intuition is that it should be changing an offset in the vtbl, without actually changing the table's size.

Well, your intuition is clearly wrong:

  • either there is a new entry in the vtable for the overrider, all following entries are moved, and the table grows,
  • or there is no new entry, and the vtable representation does not change.

Which one is true can depends on many factors.

Anyway: do not count on it.

Dorso answered 7/8, 2012 at 22:30 Comment(0)
G
0

Caution: see In C++, does overriding an existing virtual function break ABI? for a case where this logic doesn't hold true;

In my mind Mark's suggestion to use g++ -fdump-class-hierarchy would be the winner here, right after having proper regression tests


Overriding things should not change vtable layout[1]. The vtable entries itself would be in the datasegment of the library, IMHO, so a change to it should not pose a problem.

Of course, the applications need to be relinked, otherwise there is a potential for breakage if the consumer had been using direct reference to &Derived::overriddenMethod; I'm not sure whether a compiler would have been allowed to resolve that to &Base::overriddenMethod at all, but better safe than sorry.

[1] spelling it out: this presumes that the method was virtual to begin with!

Gera answered 21/4, 2011 at 15:25 Comment(4)
"_Overriding things should not change vtable layout" Wrong. It depends.Dorso
@Dorso I thought I made that very clear. Also I link to relevant resources to reach a verdict based on the actual context. Because... it depends on the actual context.Gera
"I thought I made that very clear." Not clear for me, sorry. Are you saying that, for single inheritance, adding an overrider does not change the layout of the vtable?Dorso
@Dorso yes, as that is my understanding. If that's not correct, why don't you provide an answer describing such cases so we can upvote it?Gera

© 2022 - 2024 — McMap. All rights reserved.