views:

256

answers:

5

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

+1  A: 

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.

Ukko
Yeah, if gcc doesn't see any non-inline, virtual functions in a class, it won't emit the vtable for it. http://gcc.gnu.org/faq.html#vtables
Mark B
Thanks, that helps. I just remember reading a really good writeup on this issue but Teh Great Gizoogle is failing me at the moment since I cannot come up with the right combination of keywords to find it. It is bad enough I can't keep things in my real head, when my virtual brain is leaky as well, that leaves me in quite a sorry state ;-)
Ukko
Whoohoo, I also got my first neg for an answer with this one! I admit it was not my best work, and for those interested the +1 followed by the -1 netted out to +8. I always wondered if it would work that way or if you would lose the original +10, now we know ;-)
Ukko
A: 

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.

sechastain
This is bogus reasoning. Just because a function is inline does not mean that you can't take its address. "What it would point to" is compiler's problem not user's problem. Normally, compilers generate a standalone body for a function. This is what it should have done in this case as well. In other words, there are no problems with the code/approach itself.
AndreyT
Well it is not completely bogus. When you go back to C days putting code into a header was a total recipe for disaster, things are better now; however, there are still problems with where the compiler should emit that code. Under your model including a header file will result in emitting a non-inline version of lots of methods, resulting in huge object files and lots of linker work for no gain. (This is a huge problem when templates are included.) This all comes from the root problem of file based separate compilation-- compromises were made, sorry 'bout that.
Ukko
I find passing by a `void*` and casting around quite a problem, especially when given the chain of cast: `QueueClass*` -> `void*` -> `IClassInterface*` ;)
Matthieu M.
A: 

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.

Mark Ransom
How does multiple inheritance works in real world? How are vtables created? Say - class multiple : interface1, interface2;How will vtable look for this monster??? I am really confused.
0xDEAD BEEF
@0xDEAD BEEF, see my update.
Mark Ransom
+2  A: 

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.

caspin
This means the compiler+linker are just insane!They must never allow you to create a living object at run-time whose virtual table is not fully defined.In such a case the linker could generate an "unresolved" error
valdo
The compiler is just optimizing. If you never use the function why should it build one. If the compiler emitted code for every inline function for every file, compile times would be huge, Link times would be huge as well (the linker would need to sort through reems of duplicate unreferenced functions).
caspin
The `unresolved` is normally generated when the library is loaded, not when it's created, so that you have the possibility to reference symbols from another library.
Matthieu M.
You might want to edit the first paragraph, it sounds like the compiler is emitting a function for each cpp file. That only happens if the inline fails. If it is inlined it is spliced into the body of the caller at the call site, and that can happen any number of times.
Ukko
@Ukko: I meant to say it that way. The inline keyword doesn't directly affect inlining of code, stackoverflow.com/questions/1759300. The compiler will still generate a standalone version of the function for the `vtable`, even if the function's implementation is inlined instead of called. The optimization step may then realize the standalone version is never called and remove it, but this is true of any unreferenced function. Most notably, private member functions often do not have standalone versions, because the compiler will inline them and they aren't referenced anywhere else.
caspin
Why compiler finds definition for LOL, when it is in cpp file (but only defined, but never referenced), but does not find/create vtable entry, when this same definition is in header file? Google says, that inline functions (defined in header) are never inlined for virtual keyword (because they are virtual). hmm
0xDEAD BEEF
@0xDEAD BEEF, the compiler may inline a virtual function if it is not being called through a pointer or reference. I.e. `QueueClass q; q.LOL();` the compiler knows that it will be calling QueueClass::LOL and going through the vtable would be redundant.
Mark Ransom
+2  A: 

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.

Matthieu M.
I totaly dont understand what you are talking about! :)BUT - 1) i can only have void * in exported C function because IT IS C exported function! ;)2) it is first time i hear about "C++ compiler may perform the correct pointer arithmetic depending on the layout of the objects" .. what?? :)
0xDEAD BEEF
BTW - whats wrong about my design? Well - i love C++, but shared libraries allow only C exported functions. What should i do then? Become lame C coder and give up all good thing that c++ ofers? :)
0xDEAD BEEF
The issue is that you assume that an object in C++ has a unique address, whatever the way you are referring to it. This is unfortunately not the case. I'll edit my post to add a small example with realworld values in order to illustrate it.
Matthieu M.