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

clang: no out-of-line virtual method definitions (pure abstract C++ class)

我正在尝试使用 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? 正如 Filip Roséen 所建议的那样。在我的问题中,我特别提到了纯抽象 类 (建议的副本中未提及)。我知道 -Wweak-vtables 如何与非抽象 类 一起工作,我对此很满意。在我的示例中,我在实现文件中定义了析构函数(纯抽象)。这应该可以防止 Clang 发出任何错误,即使是 -Wweak-vtables.

我最终实现了一个简单的虚拟析构函数,而不是让它成为纯虚拟的。

所以不用

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

我用

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

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

A::~A()
{}

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

作为显式声明析构函数的副作用,您不再获得默认的复制和移动操作。有关重新声明它们的示例,请参阅 。

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

当编译器看到class声明有虚函数时,会检查在class声明中是否有只声明而没有定义的虚函数。如果只有一个这样的函数,编译器肯定知道它 必须 在某处定义(否则程序不会 link),并且仅在翻译时发出 vtable承载该函数定义的单元。如果有多个这样的函数,编译器会使用一些确定性的 selection 标准选择其中一个,并且 - 关于在何处发出 vtable 的决定 - 忽略其他函数。 select这样一个单一的代表虚函数最简单的方法就是从候选集中取第一个,clang就是这样做的。

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

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

但有一个例外——纯虚析构函数!

纯虚析构函数是一种特例:

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

因此,clang 在问题示例中的警告在概念上是不合理的。

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

我们不想将 vtable 放在每个翻译单元中。所以必须有一些翻译单元的顺序,这样我们就可以说,我们将 vtable 放在 "first" 翻译单元中。如果此顺序未定义,我们会发出警告。

你在Itanium CXX ABI中找到答案。在有关虚拟表 (5.2.3) 的部分中,您会发现:

The virtual table for a class is emitted in the same object containing the definition of its key function, i.e. the first non-pure virtual function that is not inline at the point of class definition. If there is no key function, it is emitted everywhere used. The emitted virtual table includes the full virtual table group for the class, any new construction virtual tables required for subobjects, and the VTT for the class. They are emitted in a COMDAT group, with the virtual table mangled name as the identifying symbol. Note that if the key function is not declared inline in the class definition, but its definition later is always declared inline, it will be emitted in every object containing the definition.
NOTE: In the abstract, a pure virtual destructor could be used as the key function, as it must be defined even though it is pure. However, the ABI committee did not realize this fact until after the specification of key function was complete; therefore a pure virtual destructor cannot be the key function.

第二部分是对你问题的回答。纯虚拟析构函数不是关键函数。因此,不清楚该把vtable放在哪里,到处都是。结果我们收到了警告。

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

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

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

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

    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"翻译单元。

对于所有其他情况,关键函数是不属于这些类别之一的第一个 虚拟 函数,vtable 将放置在翻译单元中关键功能已定义。

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

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

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

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

  3. 仅对 class 声明使用 .h 文件。