7

I've encountered a strange case when the final keyword is added to a virtual function declaration, with its definition on a separate .cpp file.
Consider the following example:

IClass.hpp

class IClass //COM-like base interface
{
protected:
    virtual ~IClass(){} //derived classes override this

public:
    virtual void release() final;
};

dllmain.cpp (shared library)

#include "IClass.hpp"
...

void IClass::release()
{
    delete this;
}

...

main.cpp (standalone executable)

//various includes here
...

int main(int argc, char** argv)
{
    /* From "IGameEngine.hpp"
       class IGameEngine : public IClass
       {
       ...
       };
    */
    IGameEngine* engine = factoryGameEngine();
    ...
    engine->release();
    return 0;
}

As it is, GCC 4.9.2 will report an undefined reference to 'IClass::release()'
My goal is to have IClass::release() as non-overridable while having its implementation hidden inside the game engine's shared library.
Any suggestions?

bit2shift
  • 656
  • 1
  • 9
  • 17
  • Virtual functions are always odr-used unless they are pure. I believe the linker is allowed to issue a error in that case. – David G May 18 '15 at 23:39
  • 1
    I can't reproduce the issue on my gcc 4.9.2. Please add how you're building your program (compiler options etc) – dyp May 18 '15 at 23:39
  • 1
    Did you export the function from the dll? @0x49 I was under the illusion that their odr-used-ness is implementation defined. – Yakk - Adam Nevraumont May 18 '15 at 23:40
  • @dyp: the dll is built using this [makefile](https://github.com/bit2shift/r3dVoxel/blob/master/makefile) The main binary uses the same compiler flags, but the following linker flags: `-static-libgcc -static-libstdc++ -mwindows` – bit2shift May 18 '15 at 23:44
  • @Yakk: You mean `IClass::release()`? Being an interface, it's not exported from the dll, only the factory functions are exported. I was hoping `final` wouldn't mess with the virtual table consistency. – bit2shift May 18 '15 at 23:47
  • @bit2shift IIRC, in Windows, you need to explicitly export functions from a DLL. E.g. with the nonstandard `__declspec(dllexport)` – dyp May 18 '15 at 23:51
  • 1
    @Yakk [basic.def.odr]/p5: "A virtual member function is odr-used if it is not pure." Whether or not there is a linker error is what I believe to be implementation-dependent. – David G May 18 '15 at 23:51
  • @dyp COM (and alike) interfaces aren't __declspec(dllexport)'ed from the dll, read this [answer](http://stackoverflow.com/a/12855410/2748628). **Factory functions** are the only dll exports. – bit2shift May 18 '15 at 23:57
  • @dyp The comment above `IGameEngine* engine` describes it in short form. If you want the long description, see [here](https://github.com/bit2shift/r3dVoxel/blob/master/src/r3dVoxel/IGameEngine.hpp) – bit2shift May 18 '15 at 23:59
  • Sorry. Too tired :( But I think it's getting interesting: does `final` require that the function can be linked instead of called via dynamic dispatch? – dyp May 19 '15 at 00:00
  • I've came to the conclusion that the implementation needs to be together with the declaration if using `final`. Kind of a [short coming](https://github.com/bit2shift/r3dVoxel/blob/master/src/r3dVoxel/IClass.hpp). – bit2shift May 19 '15 at 00:02
  • I don't think this is within the scope of the Standard (which doesn't cover linked libraries). But from an implementer's point of view, it seems to make sense: `final` methods do not need to be dispatched dynamically, so the linker will try to call them directly. And that's not possible since it's not exported from the DLL. – dyp May 19 '15 at 00:05
  • And to answer your question, it seems so. But I want to test the scenario where the dll's IClass::release() is different from the executable's. – bit2shift May 19 '15 at 00:05
  • What's the point of a virtual function that's final in the base class anyway? Why not ditch the virtual and final aspects? – Tony Delroy May 19 '15 at 02:00
  • What does it matter what abstact interfaces do about exporting? The code above contains no abstract interfaces -- it has a class with code in it. That code must be shared at either compile or run time (with a compile time dynamic loader/stub). How are you sharing it with client code? You intend to "expose" it just via the vtable? – Yakk - Adam Nevraumont May 19 '15 at 02:42
  • @TonyD A function that's accessible through dynamic dispatch yet cannot be overriden. A function that is common to all derived interfaces and is used to call the implementation's destructor. – bit2shift May 19 '15 at 03:28
  • @Yakk Go read about how [COM](http://en.wikipedia.org/wiki/Component_Object_Model) works. – bit2shift May 19 '15 at 03:31
  • And specially, [go look how I do it](https://github.com/bit2shift/r3dVoxel/tree/master/src). – bit2shift May 19 '15 at 03:34
  • Your code isn't com, right? No idl, no coclass factories? You just want to use slightly similar runtime behaviours? Links to answwrs about pure virtual methods are worse than useless. The problem is that your header file tells clients that a call to `->release()` is guaranteed to be dispatched to one particular method: so the client calls that method directly, bypassing the vtable, and you get a linker error. Without final, the client feels it must use the vtable in how you call it, so it does not access the symbol for the method, no linker error. – Yakk - Adam Nevraumont May 19 '15 at 10:05
  • Call `engine->IClass::release();` in your `main`. Now with and without `final` you will get linker error. The fact there was no linker error prior to `final` was in a sense accidental: it was because nobody called a method directly. With `final` everyone now knows that `engine->release()` will mean `engine->IClass::release()`, and the latter is faster, so they call it. Boom, linker error. Similar things would happen if you inherited externally from `IClass` I suspect. – Yakk - Adam Nevraumont May 19 '15 at 10:10

1 Answers1

2

Did some digging regarding GCC's usage of final and it turns out virtual functions marked final get "devirtualized", an optimization step that aims at speeding up virtual calls by using static dispatch, and possibly inlining them.

That explains the linker error, as it tries to link IClass::release() into the executable but fails at finding it locally.

This "devirtualization" behaviour also appears on clang, but unlikely to happen with MSVC++


Partially related suggestion

In case you need a way to release an object through a pointer to its abstract class (or abstract base class):

  • The abstract base class needs a pure-virtual destructor
  • Provide the destructor's default definition outside the class (empty scope)
  • Implement the destructor on all derived classes, as usual

  • And if you're also dealing with shared libraries:

  • Export a pair of Malloc/Free functions from the library
  • Override the non-array new/delete operators and their respective std::nothrow versions on your library's header file
  • Call the above Malloc/Free from the overriden operators

  • Since the interface implementation(s) will reside inside the library, export a factory function for each interface you deem client-constructible.
    Just make sure exceptions aren't propagated through the gap between client and library.

    This way, the client application can use delete on an object allocated by the library's CRT, free of hassle.
    bit2shift
    • 656
    • 1
    • 9
    • 17