Compile-time static type check of virtual functions in C++
Asked Answered
J

1

6

Background

Recently, a colleague of mine ran into a problem where an old version of a header file for a library was used. The result was that the code generated for calling a virtual function in a C++ referred to the wrong offset in the virtual function lookup table for the class (the vtable).

Unfortunately, this error was not caught during compilation.

Question

All ordinary functions are linked using their mangled names which ensures the correct function (including the correct overload variant) is selected by the linker. Likewise, one could imagine that an object file or a library could contain symbolic information about the functions in the vtable for a C++ class.

Is there any way to let the C++ compiler (say g++ or Visual Studio) type check calls to virtual function during linking?

Example

Here is a simple test example. Imagine this simple header file and the associated implementation:

Base.hpp:

#ifndef BASE_HPP
#define BASE_HPP

namespace Test
{
  class Base
  {
  public:
    virtual int f() const = 0;
    virtual int g() const = 0;
    virtual int h() const = 0;
  };

  class BaseFactory
  {
  public:
    static const Base* createBase();
  };
}

#endif

Derived.cpp:

#include "Base.hpp"

#include <iostream>

using namespace std;

namespace Test
{
  class Derived : public Base
  {
  public:
    virtual int f() const
    {
      cout << "Derived::f()" << endl;
      return 1;
    }

    virtual int g() const
    {
      cout << "Derived::g()" << endl;
      return 2;
    }

    virtual int h() const
    {
      cout << "Derived::h()" << endl;
      return 3;
    }
  };

  const Base* BaseFactory::createBase()
  {
    return new Derived();
  }

}

Now, imagine that a program uses a wrong/old version of the header file where the virtual function in the middle is missing:

BaseWrong.hpp

#ifndef BASEWRONG_HPP
#define BASEWRONG_HPP

namespace Test
{
  class Base
  {
  public:
    virtual int f() const = 0;
    // Missing: virtual int g() const = 0;
    virtual int h() const = 0;
  };

  class BaseFactory
  {
  public:
    static const Base* createBase();
  };
}

#endif

So here we have the main program:

Main.cpp

// Including the _wrong_ version of the header!
#include "BaseWrong.hpp"

#include <iostream>

using namespace std;

int main()
{
  const Test::Base* base = Test::BaseFactory::createBase();
  const int fres = base->f();
  cout << "f() returned: " << fres << endl;
  const int hres = base->h();
  cout << "h() returned: " << hres << endl;
  return 0;
}

When we compile the “library” using the correct header and then compile and link the main program using the wrong header…

$ g++ -c Derived.cpp 
$ g++ Main.cpp Derived.o -o Main

…then the virtual call to h() uses the wrong index in the vtable so the call actually goes to g():

$ ./Main
Derived::f()
f() returned: 1
Derived::g()
h() returned: 2

In this small example the functions g() and h() have the same signature so the “only” thing that goes wrong is the wrong function being called (which is bad in itself and could go completely unnoticed), but if the signatures are different this can (and has seen to) lead to stack corruption – e.g., when calling a function in a DLL on Windows where Pascal calling convention is used (caller pushes arguments and callee pops them before returning).

Jaban answered 18/11, 2015 at 9:26 Comment(14)
ODR violations are generally not diagnosable. That's why the Standard doesn't require diagnostics for them, and that's why they're such a terrible failure mode.Crat
Your include guards are illegal. #229283Witkowski
@BaummitAugen Fixed.Jaban
@KerrekSB The question is whether some compilers – or rather linkers – provide diagnosis for e.g. this particular case.Jaban
@KerrekSB Actually ODR violations are diagnosable. You just need a smarter linker. The standard is however reluctant to mandate changes in the linker technology. Implementations could implement such diagnostics and drive the change if they wanted, but no one seems interested.Joelie
@n.m. Be the change you want to see in the worLD.Sinewy
@n.m. I completely agree! Therefore, my question is not whether the standard mandates diagnosis of ODR violations but rather if there are any known implementations that diagnoses this particular instance of an ODR violation with virtual functions which you easily run into when using a 3rd party C++ library.Jaban
@Jaban no, current binary file formats and mangling conventions do not support this.Joelie
@n.m. If you define struct X {int a; char b;}; in one TU and struct X {char b; int a;}; in another TU, do you expect a linker should be able to tell you that?Crat
@KerrekSB Current linkers don't do that but there's no reason why it should be impossible or even difficult to implement.Joelie
@n.m. It seems you have basically answered the question here. Could I persuade you into writing it up as an answer that I can accept?Jaban
Use override instead of virtual in your Derived class. It will check that base class has that method.Macedoine
@user3545806 or both, but you are correct – but only if you have a C++11 compliant compiler. Otherwise, it won't compile.Jaban
@n.m. Could I persuade you to write up your comments as an answer that I can then accept as the correct answer?Jaban
B
0

The short answer to your question is no. The fundamental problem is that the offsets to called functions are calculated at compile-time; therefore if your caller code was compiled with the (incorrect) header file that included "virtual int g() const", then your main.o will have all references to h() offset by the presence of g(). But your library has been compiled with the correct header file, therefore there is no function g(), thus the offset of h() in Derived.o will be different than in main.o

It's not a matter of typechecking calls to virtual functions - this is a "limitation" based on the fact that the C++ compiler does compile-time function offset calculation, and not runtime.

You can able to get around this problem by using dl_open instead of direct function calls and dynamically linking your library, instead of statically linking it.

Barnaba answered 24/2, 2016 at 5:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.