clang:没有外联虚拟方法定义(纯抽象C++类)

ban*_*ace 34 c++ clang llvm-clang clang++

我正在尝试使用Clang-3.5编译以下简单的C++代码:

test.h:

class A
{
  public:
    A();
    virtual ~A() = 0;
};
Run Code Online (Sandbox Code Playgroud)

test.cc:

#include "test.h"

A::A() {;}
A::~A() {;}
Run Code Online (Sandbox Code Playgroud)

我用来编译它的命令(Linux,uname -r:3.16.0-4-amd64):

$clang-3.5 -Weverything -std=c++11 -c test.cc
Run Code Online (Sandbox Code Playgroud)

而我得到的错误:

./test.h:1:7: warning: 'A' has no out-of-line virtual method definitions; its vtable will be emitted in every translation unit [-Wweak-vtables]
Run Code Online (Sandbox Code Playgroud)

任何暗示为什么会发出警告?虚拟析构函数根本没有内联.恰恰相反,test.cc中提供了一个外联定义.我在这里错过了什么?

编辑

我不认为这个问题是重复的: clang的-Wweak-vtables是什么意思? 正如FilipRoséen所说.在我的问题中,我特别提到纯抽象类(在建议的副本中没有提到).我知道如何-Wweak-vtables使用非抽象类,我很好.在我的例子中,我在实现文件中定义了析构函数(它是纯抽象的).这应该可以防止Clang发出任何错误,即使是-Wweak-vtables.

ove*_*eas 26

我们不希望将vtable放在每个翻译单元中.因此,必须对翻译单元进行一些排序,以便我们可以说,我们将vtable放在"第一"翻译单元中.如果未定义此顺序,我们会发出警告.

您可以在Itanium CXX ABI中找到答案.在关于虚拟表(5.2.3)的部分中,您会发现:

类的虚拟表在包含其键函数定义的同一对象中发出,即在类定义点处不是内联的第一个非纯虚函数.如果没有键功能,它会在任何地方发出.发出的虚拟表包括该类的完整虚拟表组,子对象所需的任何新构造虚拟表以及该类的VTT.它们在COMDAT组中发出,虚拟表损坏的名称作为标识符号.请注意,如果键函数未在类定义中内联声明,但稍后其定义始终以内联方式声明,则它将在包含该定义的每个对象中发出.
注意:在摘要中,纯虚拟析构函数可以用作键函数,因为它必须被定义,即使它是纯粹的.然而,ABI委员会直到关键功能的规范完成之后才意识到这一事实; 因此,纯虚拟析构函数不能成为关键函数.

第二部分是您的问题的答案.纯虚拟析构函数不是关键函数.因此,目前还不清楚vtable的放置位置,它放在任何地方.结果我们收到了警告.

您甚至可以在Clang源文档中找到此解释.

特别是警告:当所有虚拟函数属于以下类别之一时,您将收到警告:

  1. inlineA::x()在类定义中指定.

    struct A {
        inline virtual void x();
        virtual ~A() {
        }
    };
    void A::x() {
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. B :: x()在类定义中是内联的.

    struct B {
        virtual void x() {
        }
        virtual ~B() {
        }
    };
    
    Run Code Online (Sandbox Code Playgroud)
  3. C :: x()是纯虚拟的

    struct C {
        virtual void x() = 0;
        virtual ~C() {
        }
    };
    
    Run Code Online (Sandbox Code Playgroud)
  4. (属于3.)你有一个纯粹的虚拟析构函数

    struct D {
        virtual ~D() = 0;
    };
    D::~D() {
    }
    
    Run Code Online (Sandbox Code Playgroud)

    在这种情况下,可以定义排序,因为必须定义析构函数,但是,根据定义,仍然没有"第一"翻译单元.

对于所有其他情况下,所述键功能是第一虚拟不适合于这些类别中的一种功能,以及V表将被放置在键功能定义翻译单元.


Leo*_*eon 16

暂时,让我们忘记纯虚函数,并尝试理解编译器如何避免在包含多态类声明的所有翻译单元中发出vtable.

当编译器看到具有虚函数的类的声明时,它会检查是否存在仅在类声明中声明但未定义的虚函数.如果只有一个这样的函数,编译器肯定知道它必须在某处定义(否则程序将不会链接),并且仅在托管该函数定义的转换单元中发出vtable.如果有多个这样的函数,编译器会使用一些确定性选择标准来选择其中一个函数,并且 - 关于在哪里发出vtable的决定 - 忽略其他函数.选择这样一个代表性虚函数的最简单方法是从候选集中取第一个,这就是clang所做的.

因此,此优化的关键是选择一个虚拟方法,以便编译器可以保证它将在某些转换单元中遇到该方法的(单个)定义.

现在,如果类声明包含纯虚函数怎么办?程序员可以为纯虚函数提供实现,但是他没有义务!因此,纯虚函数不属于候选虚拟方法的列表,编译器可以从中选择代表虚拟方法.

但有一个例外 - 一个纯粹的虚拟析构函数!

纯虚拟析构函数是一种特殊情况:

  1. 如果您不打算从中派生其他类,则抽象类没有意义.
  2. 子类'析构函数总是调用基类'析构函数.
  3. 从具有虚析构函数的类派生的类的析构函数自动成为虚函数.
  4. 程序创建对象的所有类的所有虚函数通常都链接到最终的可执行文件中(包括可以静态证明保持未使用的虚函数,但这需要对整个程序进行静态分析).
  5. 因此,纯虚拟析构函数必须具有用户提供的定义.

因此,在问题的例子中,铿锵的警告在概念上是不合理的.

但是,从实际的角度来看,该示例的重要性是最小的,因为纯粹的虚拟析构函数很少(如果有的话)需要.我无法想象一个或多或少的现实情况,其中纯虚拟析构函数不会伴随另一个纯虚函数.但是在这样的设置中,对(虚拟)析构函数的纯度的需求完全消失,因为由于存在其他纯虚方法,类变得抽象.


Ted*_*val 11

我最终实现了一个简单的虚拟析构函数,而不是将其保留为纯虚拟析构函数.

而不是

class A {
public:
    virtual ~A() = 0;
};
Run Code Online (Sandbox Code Playgroud)

我用

class A {
public:
    virtual ~A();
};
Run Code Online (Sandbox Code Playgroud)

然后在.cpp文件中实现简单的析构函数:

A::~A()
{}
Run Code Online (Sandbox Code Playgroud)

这有效地将vtable固定到.cpp文件,而不是在多个转换单元(对象)中输出它,并成功避免-Wweak-vtables警告.

作为明确声明析构函数的副作用,您不再获得默认副本和移动操作.有关重新声明的示例,请参阅/sf/answers/2050181031/.

  • 这是一个Q和A网站.你发布了一个Q,我发布了A.其他人可能正在寻找解决方案,而不仅仅是解释. (8认同)
  • 你的建议可以简化为:**不要使用**纯抽象类,你不会得到警告.这与我的问题有什么关系?很明显,作者的意图是确实**使用**纯粹的抽象类. (4认同)