为什么使这个虚拟析构函数内联修复链接器问题?

时间:2017-08-14 14:59:33

标签: c++11 abstract-class linker-errors destructor

如果我有一个仅由纯虚析构函数组成的纯虚拟类InterfaceA,为什么我必须将析构函数定义为inline?当我尝试链接它时,我没有收到错误。

以下是一个公认的人为例子,但它说明了这一点。使用cmake和g ++不能为我编译这一点。但是,如果我按如下方式更改InterfaceA析构函数定义 - inline InterfaceA::~InterfaceA(){};则编译。

这是为什么? inline关键字有什么作用?

// InterfaceA.h, include guards ommitted for clarity
class InterfaceA
{
    public:
        virtual ~InterfaceA() = 0;
};

InterfaceA::~InterfaceA(){};

// A.h, include guards ommitted for clarity
#include "InterfaceA.h"
class A : public InterfaceA
{
    public:
        A(int val)
            : myVal(val){};
        ~A(){};

        int myVal;
};

// AUser.h, include guards ommitted for clarity
#include "InterfaceA.h"
class AUser
{
    public:
        AUser(InterfaceA& anA)
            : myA(anA){};
        ~AUser(){};

        int getVal() const;

    private:
        InterfaceA& myA;
};

// AUser.cpp
#include "AUser.h"
#include "A.h"

int AUser::getVal() const
{
    A& anA = static_cast<A&>(myA);
    return anA.myVal;
}

// main.cpp
#include "AUser.h"
#include "A.h"
#include <iostream>

int main(){
    A anA(1);
    AUser user(anA);
    std::cout << "value = " << user.getVal() << std::endl;
    return 0;
}

2 个答案:

答案 0 :(得分:1)

在头文件中定义函数时,必须使用inline关键字。如果不这样做,并且该文件包含在多个翻译单元中,则该函数将被定义两次(或更多次)。

链接器错误可能类似于“符号......是多重定义的”对吗?

如果你在类的主体中定义了成员函数,它将隐式内联,它也可以工作。

请参阅this answer

要回答问题“内联关键字有什么作用?”

在过去,它将用于询问编译器内联函数,即每当使用函数时插入代码而不是添加函数调用。最终它变成了一个简单的建议,因为编译器优化器变得更加了解哪些函数是内联候选者。目前,它几乎专门用于定义必须具有外部链接的头文件中的函数。

答案 1 :(得分:0)

inline表示允许编译器直接将代码添加到调用函数的位置。它还从外部链接中删除了函数,因此你的编译单元都有本地版本的.. pure destructor。

// InterfaceA.h, include guards ommitted for clarity
class InterfaceA
{
    public:
        virtual ~InterfaceA() = 0;
};

你声明析构函数是虚拟的,所以编译器几乎永远不会使它内联。为什么?因为虚函数是通过vtable调用的 - 虚函数系统的内部工作,vtable很可能实现为成员函数的指针数组。如果函数是内联的,它将没有地址,没有合法的指针。如果尝试获取函数地址,则编译器会默默地忽略inline关键字。另一个效果仍然存在:inline d析构函数停止对链接器可见。

它可能看起来像声明纯虚拟析构函数看起来像矛盾,但事实并非如此。纯析构函数是一种在不引起UB的情况下始终被调用的析构函数。它的存在会使类抽象,但是析构函数调用的隐式调用仍然会发生。如果你没有声明析构函数体,它会导致UB,例如Windows上的purecall异常。

如果您不需要抽象基类,那么内联定义就足够了:

class InterfaceA
{
public:
      virtual ~InterfaceA() {}
};

也被编译器视为inline,但不允许混合内联定义和纯成员声明。