2

I've come across quite weird scenario today. When directly calling a pure virtual method in Interface constructor, I get a undefined reference error.

class Interface
{
public:    
    virtual void fun() const = 0; 
    Interface(){ fun(); }
};

class A : public Interface
{
public:
    void fun() const override {};
};

int main()
{
    A a;
}

Results in:

prog.cc: In constructor 'Interface::Interface()':
prog.cc:5:22: warning: pure virtual 'virtual void Interface::fun() const' called from constructor
5 |     Interface(){ fun(); }
  |                      ^
/tmp/ccWMVIWG.o: In function `main':
prog.cc:(.text.startup+0x13): undefined reference to `Interface::fun() const'
collect2: error: ld returned 1 exit status

However, wrapping a call to fun() in a different method like this:

class Interface
{
public:    
    virtual void fun() const = 0; 
    Interface(){ callfun(); }
    virtual void callfun()
    {
        fun();
    }
};

class A : public Interface
{
public:
    void fun() const override {};
};

int main()
{
    A a;
}

Compiles just fine and (obviously) crashes with pure virtual call error. I've tested it on latest GCC 8.2.0 and 9.0.0 and Clang 8.0.0. Out of those, only GCC produces a linker error in the first case.

Wandbox links for a full working example with the error:

EDIT: I'm getting flagged for duplication, but I'm not sure how this question is duplicated. It doesn't have anything to do with dangers of calling pure virtual method (from constructor or whatnot), I'm aware of them.

I was trying to understand why the compiler permits this call in one scenario, and fails to do so in another, which was explained very well by Adam Nevraumont.

EDIT2: It seems, that even if callFun is not virtual, it still somehow prevents GCC from devirtualizing and inlining fun call. See the example below:

class Interface
{
public:    
    virtual void fun() const = 0; 
    Interface(){ callfun(); }
    void callfun()
    {
        fun();
    }
};

class A : public Interface
{
public:
    void fun() const override {};
};

int main()
{
    A a;
}
curiousguy
  • 8,038
  • 2
  • 40
  • 58
  • 1
    @FrederikDeRuyck No; the OP is well aware it is dangerous. The OP is asking why the compiler isn't catching the problem. – Yakk - Adam Nevraumont Sep 21 '18 at 14:04
  • Possible duplicate of [Calling virtual functions inside constructors](https://stackoverflow.com/questions/962132/calling-virtual-functions-inside-constructors) –  Sep 21 '18 at 14:05
  • I'm not sure how is that a duplicate ? I'm fully aware the dangers of that, I was trying to understand why the compiler can warn me about it in one case, and fails to do so in another. – Marcin Bąk Sep 21 '18 at 14:10
  • it is not a duplicate. Dont worry, flagging as **possible** duplicate is just the first step before the question gets closed as duplicate, and even then it would be possible to reopen it – 463035818_is_not_an_ai Sep 21 '18 at 14:21
  • This code doesn't have a `main`, so it cannot possibly "crash", nor should it even get to the link stage. Post real code, not links to someplace else. – Pete Becker Sep 21 '18 at 15:28
  • @PeteBecker I wasn't sure wheter to include full code, or just a snippet, because the snippet is actually where everything happens. I've updated the original post for your liking. – Marcin Bąk Sep 21 '18 at 15:55
  • @MarcinBąk -- that example is much better! Make it easy for people to cut-and-paste your code to duplicate the problem. – Pete Becker Sep 21 '18 at 16:17
  • In the older days, like GCC 5, you would not get the warning. At runtime you would get a crash with an obscure message on platforms like Aarch64 and PowerPC (x86 was usually OK). I can't find the message at the moment, but it is one of those errors you know it immediately after you suffered it once. – jww Sep 21 '18 at 16:47

1 Answers1

5

You aren't calling the pure virtual function, you are doing a lookup in the vtable for the current entry in the virtual function table for that function.

As it happens, at that point it is a pure virtual function, so you crash due to UB.

In the first case, you are getting a linker error because gcc is devirtualizing the call to fun in the ctor. A devirtualized call to fun directly calls the pure virtual method. This is possible because while constructing Interface, the compiler knows the state of the virtual function table (derived class modifications to it do not occur yet).

In the second case, the compiler can devirtualize the call to callFun from the ctor. But the call to fun from within callFun cannot be devirtualized, as callFun could be called from outside the ctor in another method. Devirtualizing it would be incorrect in the general case.

In this specific case, if the compiler devirtualized callFun and then inlined it, it could then devirtualize fun in the inlined copy. But the compiler doesn't do this, so no devirtualization occurs.

As an aside, you can implement that pure virtual function and cause every example you provided to both link and run fine.

void Interface::fun() const {}

anywhere in any .cpp file linked in will make your code link, and be correct regardless. Pure virtual doesn't mean "has no implementation" in C++, it just means "derived class must provide an override, and it is legal for me not to have an implementation".

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Aha ! I thought about the compiler trying to inline the function and failing to do so because of the lack of body, but didn't know about the devirtualization. I would've assumed that happens only with optimizations enabled. Thank you ! – Marcin Bąk Sep 21 '18 at 14:08
  • is this maybe the reason why pure virtual methods are allowed to have a definition? I still wonder why that is the case... – 463035818_is_not_an_ai Sep 21 '18 at 14:09
  • @user463035818 if it helps you in any way, I thought of pure virtual methods as a way to enforce deriving class to implement this function. You could have the deriving class call the pure virtual base method to do some calculations beforehand and the do some more on its own. – Marcin Bąk Sep 21 '18 at 14:34
  • @MarcinBąk wow cool. that makes sense. I never thought about the possibility to call the base implementation from the derived class. In such a case I always used a virtual (but not pure) method – 463035818_is_not_an_ai Sep 21 '18 at 14:45
  • @Yakk One more thing, if we remove virtual from callFun, it still passes compilation, even with -O3 . It seems that wrapping the pure virtual function in a separate call somehow prevents compiler from devirtualizing it entirely. https://wandbox.org/permlink/38JEGGyA3hfAfPAS – Marcin Bąk Sep 21 '18 at 17:36
  • 2
    @MarcinBąk Yes, because unless inlined, you cannot devirtualize. You can devirtualize calls from within the ctor, because you know the exact state of the vtable in the ctor; functions called in the ctor could be called from elsewhere, so devirtualization requires them to be first inlined into the ctor (and even then the optimizer might not do a devirtualization pass after an inline pass) – Yakk - Adam Nevraumont Sep 21 '18 at 17:47