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

后端 未结 4 1954
抹茶落季
抹茶落季 2020-11-29 22:00

I\'m trying to compile the following simple C++ code using Clang-3.5:

test.h:

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

相关标签:
4条回答
  • 2020-11-29 22:02

    For a moment, let's forget about pure virtual functions and try to understand how the compiler can avoid emitting the vtable in all translation units that include the declaration of a polymorphic class.

    When the compiler sees the declaration of a class with virtual functions, it checks whether there are virtual functions that are only declared but not defined inside the class declaration. If there is exactly one such function, the compiler knows for sure that it must be defined somewhere (otherwise the program will not link), and emits the vtable only in the translation unit hosting the definition of that function. If there are multiple such functions, the compiler choses one of them using some deterministic selection criteria and - with regard to the decision of where to emit the vtable - ignores the other ones. The simplest way to select such a single representative virtual function is to take the first one from the candidate set, and this is what clang does.

    So, the key to this optimization is to select a virtual method such that the compiler can guarantee that it will encounter a (single) definition of that method in some translation unit.

    Now, what if the class declaration contains pure virtual functions? A programmer can provide an implementation for a pure virtual function but (s)he is not obliged to! Therefore pure virtual functions do not belong to the list of candidate virtual methods from which the compiler can select the representative one.

    But there is one exception - a pure virtual destructor!

    A pure virtual destructor is a special case:

    1. An abstract class doesn't make sense if you are not going to derive other classes from it.
    2. A subclass' destructor always calls the base class' destructor.
    3. The destructor of a class deriving from a class with a virtual destructor is automatically a virtual function.
    4. All virtual functions of all classes, that the program creates objects of, are usually linked into the final executable (including the virtual functions that can be statically proved to remain unused, though that would require static analysis of the full program).
    5. Therefore a pure virtual destructor must have a user-provided definition.

    Thus, clang's warning in the question's example is not conceptually justified.

    However, from the practical point of view the importance of that example is minimal, since a pure virtual destructor is rarely, if at all, needed. I can't imagine a more or less realistic case where a pure virtual destructor won't be accompanied by another pure virtual function. But in such a setup the need for the pureness of the (virtual) destructor completely disappears, since the class becomes abstract due to the presence of other pure virtual methods.

    0 讨论(0)
  • 2020-11-29 22:09

    This can be solved in three ways.

    1. Use at least one virtual function which is not inline. Defining a virtual destructor is also alright as far as it is not an inline function.

    2. Disable the warning as shown below.

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

    3. Use only .h files for class declarations.
    0 讨论(0)
  • 2020-11-29 22:19

    I ended up implementing a trivial virtual destructor, rather than leaving it pure virtual.

    So instead of

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

    I use

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

    Then implement the trivial destructor in a .cpp file:

    A::~A()
    {}
    

    This effectively pins the vtable to the .cpp file, instead of outputting it in multiple translation units (objects), and successfully avoids the -Wweak-vtables warning.

    As a side effect of explicitly declaring the destructor you no longer get the default copy and move operations. See https://stackoverflow.com/a/29288300/954 for an example where they are redeclared.

    0 讨论(0)
  • 2020-11-29 22:24

    We don't want to place the vtable in each translation unit. So there must be some ordering of translation units, such that we can say then, that we place the vtable in the "first" translation unit. If this ordering is undefined, we emit the warning.

    You find the answer in the Itanium CXX ABI. In the section about virtual tables (5.2.3) you find:

    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.

    The second section is the answer to your question. A pure virtual destructor is no key function. Therefore, it is unclear where to place the vtable and it is placed everywhere. As a consequence we get the warning.

    You will even find this explanation in the Clang source documentation.

    So specifically to the warning: You will get the warning when all of your virtual functions belong to one of the following categories:

    1. inline is specified for A::x() in the class definition.

      struct A {
          inline virtual void x();
          virtual ~A() {
          }
      };
      void A::x() {
      }
      
    2. B::x() is inline in the class definition.

      struct B {
          virtual void x() {
          }
          virtual ~B() {
          }
      };
      
    3. C::x() is pure virtual

      struct C {
          virtual void x() = 0;
          virtual ~C() {
          }
      };
      
    4. (Belongs to 3.) You have a pure virtual destructor

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

      In this case, the ordering could be defined, because the destructor must be defined, nevertheless, by definition, there is still no "first" translation unit.

    For all other cases, the key function is the first virtual function that does not fit to one of these categories, and the vtable will be placed in the translation unit where the key function is defined.

    0 讨论(0)
提交回复
热议问题