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

时间:2015-02-28 20:25:34

标签: c++ clang clang++ llvm-clang

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

test.h:

class A
{
  public:
    A();
    virtual ~A() = 0;
};

test.cc:

#include "test.h"

A::A() {;}
A::~A() {;}

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

$clang-3.5 -Weverything -std=c++11 -c test.cc

我得到的错误:

./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]

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

修改

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

4 个答案:

答案 0 :(得分:17)

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

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

  

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

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

您甚至可以在Clang source documentation中找到此解释。

特别针对警告:当所有虚拟功能属于以下类别之一时,您将收到警告:

  1. inline在类定义中为A::x()指定。

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

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

    struct C {
        virtual void x() = 0;
        virtual ~C() {
        }
    };
    
  4. (属于3.)您有一个纯虚拟析构函数

    struct D {
        virtual ~D() = 0;
    };
    D::~D() {
    }
    

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

  5. 对于所有其他情况,键功能是第一个虚拟功能,它不适合这些类别之一,而vtable将被放置在定义键功能的翻译单元中。

答案 1 :(得分:14)

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

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

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

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

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

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

  1. 如果你不打算从中派生出其他类,那么抽象类是没有意义的。
  2. 子类'析构函数总是调用基类'析构函数。
  3. 从具有虚析构函数的类派生的类的析构函数自动成为虚函数。
  4. 程序创建对象的所有类的所有虚函数都通常链接到最终的可执行文件中(包括可以静态证明保持未使用的虚函数,但这需要静态分析完整的程序)。
  5. 因此,纯虚拟析构函数必须具有用户提供的定义。
  6. 因此,在问题的例子中,铿锵的警告在概念上是不合理的。

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

答案 2 :(得分:9)

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

所以而不是

class A {
public:
    virtual ~A() = 0;
};

我用

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

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

A::~A()
{}

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

作为明确声明析构函数的副作用,您不再获得默认副本和移动操作。有关重新声明的示例,请参阅https://stackoverflow.com/a/29288300/954

答案 3 :(得分:2)

这可以通过三种方式解决。

  1. 至少使用一个非内联的虚函数。只要不是内联函数,就可以定义虚拟析构函数。

  2. 禁用警告,如下所示。

    #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wweak-vtables" class ClassName : public Parent { ... }; #pragma clang diagnostic pop

  3. 仅将.h文件用于类声明。
相关问题