为什么在没有主体的情况下调用纯虚拟方法不会导致链接器错误?

时间:2018-09-21 13:57:23

标签: c++ constructor undefined-behavior virtual-functions pure-virtual

我今天遇到了很奇怪的情况。在接口构造函数中直接调用纯虚拟方法时,出现未定义的引用错误。

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

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

int main()
{
    A a;
}

结果:

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

但是,将调用fun()封装为其他方法如下:

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;
}

编译得很好,并且(显然)由于纯虚拟调用错误而崩溃。 我已经在最新的GCC 8.2.0和9.0.0和Clang 8.0.0上进行了测试。在这些情况中,只有GCC在第一种情况下会产生链接器错误。

Wandbox链接显示了完整的工作示例,错误为:

编辑: 我被标记为重复,但是我不确定这个问题是如何重复的。我知道它们与调用纯虚拟方法(从构造函数或其他方法)的危险没有任何关系。

我试图理解为什么编译器在一种情况下允许此调用,而在另一种情况下不允许这样做,Adam Nevraumont对此进行了很好的解释。

EDIT2: 看来,即使callFun不是虚拟的,它仍然以某种方式阻止了GCC对fun的调用进行虚拟化和内联。请参见下面的示例:

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;
}

1 个答案:

答案 0 :(得分:5)

您不是在调用纯虚函数,而是在vtable中对该函数的虚函数表中的当前条目进行查找。

碰巧的是,这只是一个纯虚函数,因此您由于UB而崩溃。

在第一种情况下,您会收到链接器错误,因为gcc正在取消对ctor中对fun的调用的虚拟化。对fun的非虚拟化调用直接调用纯虚拟方法。之所以可能这样做,是因为在构造Interface时,编译器知道虚函数表的状态(对它的派生类修改尚未发生)。

在第二种情况下,编译器可以将ctor对callFun的调用虚拟化。但是无法撤消从fun内部对callFun的调用,因为可以使用另一种方法从ctor外部调用callFun。将其取消虚拟化在一般情况下是不正确的

在这种特定情况下,如果编译器对callFun进行虚拟化,然后对其进行内联,则可以在内联副本中对fun进行虚拟化。但是编译器不会这样做,因此不会发生非虚拟化。

顺便说一句,您可以实现该纯虚函数,并使您提供的每个示例都链接并正常运行。

void Interface::fun() const {}

在链接的任何.cpp文件中的任何位置都将使您的代码链接,并且无论如何都是正确的。纯虚拟语言在C ++中并不意味着“没有实现”,而只是意味着“派生类必须提供重写,并且对我来说,没有实现是合法的。”