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

Why does making this virtual destructor inline fix a linker issue?

如果我有一个纯虚拟 class InterfaceA,它只包含一个纯虚拟析构函数,为什么我必须将析构函数定义为 inline?我没有 我尝试 link 时出现错误。

下面是一个公认的人为设计的例子,但它说明了这一点。使用 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;
}

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

链接器错误可能类似于 "Symbol ... is multiply defined" 对吗?

如果您在 class 的主体中定义成员函数,它将隐式内联并且也可以工作。

this answer

回答问题"What does the inline keyword do?":

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

inline 表示允许编译器直接在调用函数的地方添加代码。它还从外部链接中删除函数,因此您的两个编译单元都将具有本地版本的..纯析构函数。

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

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

声明纯虚拟析构函数可能看起来很矛盾,但事实并非如此。纯析构函数是一种总是会被调用而不会导致 UB 的析构函数。它的存在会使 class 变得抽象,但是析构函数调用序列中的隐式调用仍然会发生。如果你没有声明析构函数体,它会导致一个 UB,例如Windows 上的 purecall 异常。

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

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

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