【问题标题】:clang: no out-of-line virtual method definitions (pure abstract C++ class)clang:没有外线的虚拟方法定义(纯抽象 C++ 类)
【发布时间】:2015-05-01 10:17:00
【问题描述】:

我正在尝试使用 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? 正如菲利普·罗森建议的那样。在我的问题中,我特别提到了纯抽象类(建议的副本中没有提到)。我知道-Wweak-vtables 是如何与非抽象类一起工作的,我很好。在我的示例中,我在实现文件中定义了析构函数(纯抽象)。这应该可以防止 Clang 发出任何错误,即使是 -Wweak-vtables

【问题讨论】:

  • @gha.st 确实如此,但有什么不同呢?我在实现更复杂的抽象类时遇到了这个问题,但这在这里无关紧要。上面的代码应该还是没问题的吧?
  • 这不正是你在 3 小时后问的 this other question 的骗局吗?
  • @Barry 我认为这是两者中更好的问题,所以这是我赏金的。另一个问题解释说,在询问那个人时,这个人被作为另一个人的欺骗而关闭,但后来被重新打开。
  • 这可以通过两种方式解决。
  • 这可以通过三种方式解决。 1. 使用至少一个非内联的虚函数。只要它不是内联函数,定义一个虚拟析构函数也是可以的。 2. 禁用警告,如下所示。 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wweak-vtables" class ClassName : public Parent { ... }; #pragma clang 诊断弹出

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


【解决方案1】:

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

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

类的虚表在包含其关键函数定义的同一对象中发出,即第一个非纯虚函数在类定义点不是内联的。如果没有 key 函数,它会在任何使用的地方发出。发出的虚拟表包括该类的完整虚拟表组、子对象所需的任何新构造虚拟表以及该类的 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() {
    }
    

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

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

【讨论】:

    【解决方案2】:

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

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

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

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

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

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

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

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

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

    【讨论】:

    • 另见Herb Sutter's thoughts 纯虚析构函数。我可以想象纯虚析构函数作为唯一虚方法有用的唯一情况是,当您想要一个指针集合时,使用dynamic_cast 来处理指针。
    【解决方案3】:

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

    所以不是

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

    我用

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

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

    A::~A()
    {}
    

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

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

    【讨论】:

    • 您的建议可以简化为:不要使用纯抽象类,您不会收到警告。这与我的问题有什么关系?很明显,作者的意图确实是使用一个纯抽象类。
    • 它在概念上仍然是一个纯抽象基类,但没有复制 vtable 的副作用。
    • 我第一句话就是这么说的。我正在提供一个可能的解决方案。我在您转发此问题时提供了两种可能的解决方案的更长描述:stackoverflow.com/questions/28788353/… - 如果您想保持纯粹的抽象,我在其中包含了一个禁用警告的示例。
    • 问题是:“任何提示为什么会发出警告?”。我没有要求解决方案,您也没有提供解决方案 - 这是前面提到的解决方法stackoverflow.com/questions/28788353/…
    • 这是一个问答网站。你发布了一个 Q,我发布了一个 A。其他人可能正在寻找解决方案,而不仅仅是解释。
    【解决方案4】:

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

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

    2. 如下所示禁用警告。

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

    3. 仅将 .h 文件用于类声明。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-01-15
      • 2012-10-01
      • 2016-01-25
      • 2019-01-01
      • 2016-06-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多