Odd behaviour of final on a virtual function
Asked Answered
T

1

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?

Thomas answered 18/5, 2015 at 23:30 Comment(20)
Virtual functions are always odr-used unless they are pure. I believe the linker is allowed to issue a error in that case.Music
I can't reproduce the issue on my gcc 4.9.2. Please add how you're building your program (compiler options etc)Shankle
Did you export the function from the dll? @0x49 I was under the illusion that their odr-used-ness is implementation defined.Suttles
@dyp: the dll is built using this makefile The main binary uses the same compiler flags, but the following linker flags: -static-libgcc -static-libstdc++ -mwindowsThomas
@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.Thomas
@Thomas IIRC, in Windows, you need to explicitly export functions from a DLL. E.g. with the nonstandard __declspec(dllexport)Shankle
@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.Music
@Shankle COM (and alike) interfaces aren't __declspec(dllexport)'ed from the dll, read this answer. Factory functions are the only dll exports.Thomas
@Shankle The comment above IGameEngine* engine describes it in short form. If you want the long description, see hereThomas
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?Shankle
I've came to the conclusion that the implementation needs to be together with the declaration if using final. Kind of a short coming.Thomas
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.Shankle
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.Thomas
What's the point of a virtual function that's final in the base class anyway? Why not ditch the virtual and final aspects?Dunk
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?Suttles
@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.Thomas
@Yakk Go read about how COM works.Thomas
And specially, go look how I do it.Thomas
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.Suttles
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.Suttles
T
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.
    Thomas answered 23/5, 2015 at 21:2 Comment(0)

    © 2022 - 2024 — McMap. All rights reserved.