Is it possible to break code by adding a new virtual function in the base class?
Asked Answered
P

4

3

Is it possible to have the observed behavior of a program changed by simply adding a new virtual function to a base class? I mean that no other change must be made to the code.

Pamphlet answered 29/7, 2016 at 11:19 Comment(4)
Of course. The list of possible ways to break existing code, in C++, is an endless list.Harmaning
@SamVarshavchik This question and answer is an illustration requested elsewhere.Pamphlet
One example is if you add a pure virtual function. No implementation in the instantiated derived class(es) will break compilation.Hammons
Related: What is the opposite of c++ override / final specifier?Emeraldemerge
D
2
#include <stdlib.h>

struct A {
#if ADD_TO_BASE
  virtual void foo() { }
#endif
};

struct B : A {
  void foo() { }
};

struct C : B {
  void foo() { abort(); }
};

int main() {
  C c;
  B& b = c;
  b.foo();
}

Without the virtual function the base class b.foo() is a non-virtual call to B::foo():

$ g++ virt.cc
$ ./a.out

With the virtual in the base class it is a virtual call to C::foo():

$ g++ virt.cc -DADD_TO_BASE
$ ./a.out
Aborted (core dumped)

You can also get nasty undefined behaviour due to binary incompatibility, because adding a virtual function to a non-polymorphic base class changes its size and layout (requiring all other translation units that use it to be recompiled).

Adding a new virtual function to an already polymorphic base class changes the layout of the vtable, either adding a new entry at the end, or changing the position of other functions if added in the middle (and even if added at the end of the base vtable, that's in the middle of the vtable for any derived classes which add new virtual functions). That means that already compiled code that uses the vtable might end up calling the wrong function, because it uses the wrong slot in the vtable.

The binary incompatibilities can be fixed by recompiling all the relevant code, but silent changes in behaviour like the example at the top can't be fixed simply by recompiling.

Dar answered 29/7, 2016 at 11:52 Comment(0)
P
5

The following program prints OK. Uncomment the virtual function in B and it will start printing CRASH!.

#include <iostream>

struct B
{
    //virtual void bar() {}
};

struct D : B
{
    void foo() { bar(); }
    void bar() { std::cout << "OK" << std::endl; }
};

struct DD : D
{
    void bar() { std::cout << "CRASH!" << std::endl; }
};

int main()
{
    DD d;
    d.foo();
    return 0;
}

The problem is that after a virtual function B::bar() is introduced the binding of the call to bar() in D::foo() changes from static to dynamic.

Pamphlet answered 29/7, 2016 at 11:19 Comment(4)
But this is the purpose of virtual functions in the first place.... There is nothing surprising here :-). As far as you have a function signature in a derived class that is same, but virtual as in one of the base(s), it is automatically an override. Mistakes from slightly different signatures is partly what brought about the override keywordArcanum
@Arcanum This question and answer is an illustration requested elsewhere.Pamphlet
@WhiZTiM: No there is nothing surprising, but it does provide an example to prove that introducing a virtual function in a base class can break previous working code (which is the question that the OP asked).Bouie
I understand. Good point... Upvoted. :-). But @Leon, I think you should also modify your answer to point out binary incompatibility of modifying a base class.Arcanum
B
5

Binary incompatibility.

If you have an externally loadable module (i.e a DLL) then which uses the old definition of the base class you will have problems. Or if the loader program have the old definition and the DLL have the new it's the same problem. This is also a problem if you for some reason save objects in files using raw binary copying (not any kind of serialization).

This have nothing to do with with the C++ specification of virtual functions, but how most compilers implement them.

Generally speaking, if the "interface" of a class changes (base class or not) then you should recompile everything which uses that class.

Bidget answered 29/7, 2016 at 11:27 Comment(1)
It's worse than that, see the answer from Leon - which doesn't require anything outside the C++ standard.Bouie
O
2

When the API is changed in a backwards incompatible manner, the code that depends on the earlier version of the API is no longer guaranteed to work.

All derived classes depend on the API of their base classes.

Addition of a virtual function is a backwards incompatible change. Leon's answer shows a fine example of how the API breakage can manifest itself.

Therefore yes, addition of a virtual function can break the program, unless the dependant parts are fixed to work with the new API. This means that whenever a virtual function is added, one should inspect all derived classes, and make sure that the meaning of their respective API has not been changed by the addition.

Opiate answered 29/7, 2016 at 11:28 Comment(2)
You assert that adding a virtual function is a backwards incompatible change, but you don't provide any evidence of that assertion. Given that the question can be rephrased as "is adding a virtual function to a base class a backwards incompatible change", just saying "yes" is not very helpful.Bouie
@MartinBonner that change would alter the meaning of the question (a backwards incompatible change, if you will) albeit subtly. I agree, my assertion should be accompanied by an example. Leon has already posted a fine example, and I don't see any need to repeat it. However, I shall refer to it to back up my assertion.Opiate
D
2
#include <stdlib.h>

struct A {
#if ADD_TO_BASE
  virtual void foo() { }
#endif
};

struct B : A {
  void foo() { }
};

struct C : B {
  void foo() { abort(); }
};

int main() {
  C c;
  B& b = c;
  b.foo();
}

Without the virtual function the base class b.foo() is a non-virtual call to B::foo():

$ g++ virt.cc
$ ./a.out

With the virtual in the base class it is a virtual call to C::foo():

$ g++ virt.cc -DADD_TO_BASE
$ ./a.out
Aborted (core dumped)

You can also get nasty undefined behaviour due to binary incompatibility, because adding a virtual function to a non-polymorphic base class changes its size and layout (requiring all other translation units that use it to be recompiled).

Adding a new virtual function to an already polymorphic base class changes the layout of the vtable, either adding a new entry at the end, or changing the position of other functions if added in the middle (and even if added at the end of the base vtable, that's in the middle of the vtable for any derived classes which add new virtual functions). That means that already compiled code that uses the vtable might end up calling the wrong function, because it uses the wrong slot in the vtable.

The binary incompatibilities can be fixed by recompiling all the relevant code, but silent changes in behaviour like the example at the top can't be fixed simply by recompiling.

Dar answered 29/7, 2016 at 11:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.