Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why C++ virtual function defined in header may not be compiled and linked in vtable?

Situation is following. I have shared library, which contains class definition -

QueueClass : IClassInterface
{
   virtual void LOL() { do some magic}
}

My shared library initialize class member

QueueClass *globalMember = new QueueClass();

My share library export C function which returns pointer to globalMember -

void * getGlobalMember(void) { return globalMember;}

My application uses globalMember like this

((IClassInterface*)getGlobalMember())->LOL();

Now the very uber stuff - if i do not reference LOL from shared library, then LOL is not linked in and calling it from application raises exception. Reason - VTABLE contains nul in place of pointer to LOL() function.

When I move LOL() definition from .h file to .cpp, suddenly it appears in VTABLE and everything works just great. What explains this behavior?! (gcc compiler + ARM architecture_)

like image 385
0xDEAD BEEF Avatar asked May 24 '10 15:05

0xDEAD BEEF


2 Answers

The linker is the culprit here. When a function is inline it has multiple definitions, one in each cpp file where it is referenced. If your code never references the function it is never generated.

However, the vtable layout is determined at compile time with the class definition. The compiler can easily tell that the LOL() is a virtual function and needs to have an entry in the vtable.

When it gets to link time for the app it tries to fill in all the values of the QueueClass::_VTABLE but doesn't find a definition of LOL() and leaves it blank(null).

The solution is to reference LOL() in a file in the shared library. Something as simple as &QueueClass::LOL;. You may need to assign it to a throw away variable to get the compiler to stop complaining about statements with no effect.

like image 53
deft_code Avatar answered Sep 24 '22 23:09

deft_code


I disagree with @sechastain.

Inlining is far from being automatic. Whether or not the method is defined in place or a hint (inline keyword or __forceinline) is used, the compiler is the only one to decide if the inlining will actually take place, and uses complicated heuristics to do so. One particular case however, is that it shall not inline a call when a virtual method is invoked using runtime dispatch, precisely because runtime dispatch and inlining are not compatible.

To understand the precision of "using runtime dispatch":

IClassInterface* i = /**/;
i->LOL();                   // runtime dispatch
i->QueueClass::LOL();       // compile time dispatch, inline is possible

@0xDEAD BEEF: I find your design brittle to say the least.

The use of C-Style casts here is wrong:

QueueClass* p = /**/;
IClassInterface* q = p;

assert( ((void*)p) == ((void*)q) ); // may fire or not...

Fundamentally there is no guarantee that the 2 addresses are equal: it is implementation defined, and unlikely to resist change.

I you wish to be able to safely cast the void* pointer to a IClassInterface* pointer then you need to create it from a IClassInterface* originally so that the C++ compiler may perform the correct pointer arithmetic depending on the layout of the objects.

Of course, I shall also underline than the use of global variables... you probably know it.

As for the reason of the absence ? I honestly don't see any apart from a bug in the compiler/linker. I've seen inlined definition of virtual functions a few times (more specifically, the clone method) and it never caused issues.

EDIT: Since "correct pointer arithmetic" was not so well understood, here is an example

struct Base1 { char mDum1; };

struct Base2 { char mDum2; };

struct Derived: Base1, Base2 {};

int main(int argc, char* argv[])
{
  Derived d;
  Base1* b1 = &d;
  Base2* b2 = &d;

  std::cout << "Base1: " << b1
          << "\nBase2: " << b2
          << "\nDerived: " << &d << std::endl;

  return 0;
}

And here is what was printed:

Base1: 0x7fbfffee60
Base2: 0x7fbfffee61
Derived: 0x7fbfffee60

Not the difference between the value of b2 and &d, even though they refer to one entity. This can be understood if one thinks of the memory layout of the object.

Derived
Base1     Base2
+-------+-------+
| mDum1 | mDum2 |
+-------+-------+

When converting from Derived* to Base2*, the compiler will perform the necessary adjustment (here, increment the pointer address by one byte) so that the pointer ends up effectively pointing to the Base2 part of Derived and not to the Base1 part mistakenly interpreted as a Base2 object (which would be nasty).

This is why using C-Style casts is to be avoided when downcasting. Here, if you have a Base2 pointer you can't reinterpret it as a Derived pointer. Instead, you will have to use the static_cast<Derived*>(b2) which will decrement the pointer by one byte so that it correctly points to the beginning of the Derived object.

Manipulating pointers is usually referred to as pointer arithmetic. Here the compiler will automatically perform the correct adjustment... at the condition of being aware of the type.

Unfortunately the compiler cannot perform them when converting from a void*, it is thus up to the developer to make sure that he correctly handles this. The simple rule of thumb is the following: T* -> void* -> T* with the same type appearing on both sides.

Therefore, you should (simply) correct your code by declaring: IClassInterface* globalMember and you would not have any portability issue. You'll probably still have maintenance issue, but that's the problem of using C with OO-code: C is not aware of any object-oriented stuff going on.

like image 21
Matthieu M. Avatar answered Sep 23 '22 23:09

Matthieu M.