Why C++ virtual function defined in header may not

2019-02-13 14:07发布

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_)

5条回答
老娘就宠你
2楼-- · 2019-02-13 14:40

Functions defined in header files are in-lined on usage. They're not compiled as part of the library; instead where the call is made, the code of the function simply replaces the code of the call, and that is what gets compiled.

So, I'm not surprised to see that you are not finding a v-table entry (what would it point to?), and I'm not surprised to see that moving the function definition to a .cpp file suddenly makes things work. I'm a little surprised that creating an instance of the object with a call in the library makes a difference, though.

I'm not sure if it's haste on your part, but from the code provided IClassInterface does not necessarily contain LOL, only QueueClass. But you're casting to a IClassInterface pointer to make the LOL call.

查看更多
唯我独甜
3楼-- · 2019-02-13 14:46

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.

查看更多
冷血范
4楼-- · 2019-02-13 14:49

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.

查看更多
▲ chillily
5楼-- · 2019-02-13 14:58

My guess is that GCC is taking the opportunity to inline the call to LOL. I'll see if I can find a reference for you on this...

I see sechastain beat me to a more thorough description and I could not google up the reference I was looking for. So I'll leave it at that.

查看更多
Lonely孤独者°
6楼-- · 2019-02-13 15:01

If this example is simplified, and your actual inheritance tree uses multiple inheritance, this might be easily explained. When you do a typecast on an object pointer, the compiler needs to adjust the pointer so that the proper vtable is referenced. Because you're returning a void *, the compiler doesn't have the necessary information to do the adjustment.

Edit: There is no standard for C++ object layout, but for one example of how multiple inheritance might work see this article from Bjarne Stroustrup himself: http://www-plan.cs.colorado.edu/diwan/class-papers/mi.pdf

If this is indeed your problem, you might be able to fix it with one simple change:

IClassInterface *globalMember = new QueueClass();

The C++ compiler will do the necessary pointer modifications when it makes the assignment, so that the C function can return the correct pointer.

查看更多
登录 后发表回答