Подтвердить что ты не робот

Clang: нет определений виртуального метода вне сети (чистый абстрактный класс С++)

Я пытаюсь скомпилировать следующий простой код на С++ с помощью Clang-3.5:

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. Что мне здесь не хватает?

Изменить

Я не думаю, что этот вопрос является дубликатом: В чем смысл clang -Wweak-vtables? как предположил Филипп Розен. В моем вопросе я конкретно ссылаюсь на чистые абстрактные классы (не упомянутые в предлагаемом дубликате). Я знаю, как -Wweak-vtables работает с не-абстрактными классами, и я в порядке с ним. В моем примере я определяю деструктор (который является чисто абстрактным) в файле реализации. Это должно помешать Clang испускать какие-либо ошибки даже при -Wweak-vtables.

4b9b3361

Ответ 1

Мы не хотим размещать vtable в каждой единицы перевода. Таким образом, должен быть некоторый порядок единиц перевода, так что мы можем сказать тогда, что мы поместим vtable в "первую" единицу перевода. Если это упорядочение undefined, мы выдаем предупреждение.

Вы найдете ответ в Itanium CXX ABI. В разделе о виртуальных таблицах (5.2.3) вы найдете:

Виртуальная таблица для класса испускается в том же объекте, который содержит определение его ключевой функции, то есть первой нечистой виртуальной функции, которая не является встроенной в точке определения класса. Если ключевой функции нет, она испускается везде. Испущенная виртуальная таблица включает в себя полную группу виртуальных таблиц для класса, любые новые виртуальные таблицы построения, необходимые для подобъектов, и VTT для класса. Они испускаются в группе COMDAT, а имя виртуальной таблицы изменено как идентификационный символ. Обратите внимание, что если ключевая функция не объявлена ​​встроенной в определение класса, но ее определение позже всегда объявляется встроенным, оно будет испускаться в каждом объекте, содержащем определение.
ПРИМЕЧАНИЕ. В реферате в качестве ключевой функции можно использовать чистый виртуальный деструктор, поскольку он должен быть определен, даже если он чист. Однако комитет ABI не осознавал этого факта до тех пор, пока спецификация ключевой функции не была завершена; , поэтому чистый виртуальный деструктор не может быть ключевой функцией.

Второй раздел - ответ на ваш вопрос. Чистый виртуальный деструктор не является ключевой функцией. Поэтому неясно, где разместить vtable, и оно размещается повсюду. Как следствие, мы получаем предупреждение.

Вы даже найдете это объяснение в Документация источника Clang.

Так специально для предупреждения: вы получите предупреждение, когда все ваши виртуальные функции относятся к одной из следующих категорий:

  • inline указывается для A::x() в определении класса.

    struct A {
        inline virtual void x();
        virtual ~A() {
        }
    };
    void A::x() {
    }
    
  • B:: x() является встроенным в определение класса.

    struct B {
        virtual void x() {
        }
        virtual ~B() {
        }
    };
    
  • C:: x() является чистым виртуальным

    struct C {
        virtual void x() = 0;
        virtual ~C() {
        }
    };
    
  • (Принадлежит к 3.) У вас есть чистый виртуальный деструктор

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

    В этом случае порядок может быть определен, так как деструктор должен быть определен, тем не менее, по определению, до сих пор нет "первой" единицы перевода.

Для всех остальных случаев ключевая функция является первой виртуальной функцией, которая не соответствует одной из этих категорий, а vtable будет помещена в блок перевода, где определена ключевая функция.

Ответ 2

На мгновение забудьте о чистых виртуальных функциях и попытайтесь понять, как компилятор может избежать испускания vtable во всех единицах перевода, которые включают объявление полиморфного класса.

Когда компилятор видит объявление класса с виртуальными функциями, он проверяет, есть ли виртуальные функции, которые только объявлены, но не определены внутри объявления класса. Если есть ровно одна такая функция, компилятор точно знает, что он должен быть определен где-то (иначе программа не будет ссылаться) и испускает vtable только в модуле перевода, на котором размещается определение этой функции. Если существует несколько таких функций, компилятор выбирает один из них с использованием некоторых критериев детерминированного отбора и - в отношении решения о том, где испускать vtable - игнорирует другие. Самый простой способ выбрать такую ​​единственную репрезентативную виртуальную функцию - взять первый из набора кандидатов, и это то, что делает clang.

Итак, ключом к этой оптимизации является выбор виртуального метода, так что компилятор может гарантировать, что он столкнется с (единственным) определением этого метода в некоторой единицы перевода.

Теперь, что, если объявление класса содержит чистые виртуальные функции? Программист может обеспечить реализацию для чистой виртуальной функции, но (s) он не обязан! Поэтому чистые виртуальные функции не относятся к списку потенциальных виртуальных методов, из которых компилятор может выбрать представительский.

Но есть одно исключение - чистый виртуальный деструктор!

Чистым виртуальным деструктором является частный случай:

  • Абстрактный класс не имеет смысла, если вы не собираетесь извлекать из него другие классы.
  • Деструктор подкласса всегда вызывает деструктор базового класса.
  • Деструктор класса, происходящего из класса с виртуальным деструктором, автоматически является виртуальной функцией.
  • Все виртуальные функции всех классов, которые программа создает объекты, обычно связаны с окончательным исполняемым файлом (включая виртуальные функции, которые могут быть статически доказаны, чтобы оставаться неиспользованными, хотя для этого потребовался бы статический анализ полной программы).
  • Поэтому чистый виртуальный деструктор должен иметь предоставленное пользователем определение.

Таким образом, предупреждение clang в примере вопроса не является концептуально оправданным.

Однако с практической точки зрения важность этого примера минимальна, поскольку чистый виртуальный деструктор редко, если вообще нужен, необходим. Я не могу представить более или менее реалистичный случай, когда чистый виртуальный деструктор не будет сопровождаться другой чистой виртуальной функцией. Но в такой установке потребность в чистоте (виртуального) деструктора полностью исчезает, поскольку класс становится абстрактным из-за наличия других чистых виртуальных методов.

Ответ 3

Я закончил реализацию тривиального виртуального деструктора, вместо того, чтобы оставить его чистым виртуальным.

Поэтому вместо

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

я использую

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

Затем реализуем тривиальный деструктор в файле.cpp:

A::~A()
{}

Это эффективно связывает vtable с файлом.cpp, а не выводит его в несколько единиц перевода (объектов) и успешно избегает предупреждения -Wweak-vtables.

В качестве побочного эффекта явного объявления деструктора вы больше не получаете операции копирования и перемещения по умолчанию. См. fooobar.com/questions/264042/... для примера, где они были обновлены.

Ответ 4

Это можно решить тремя способами.

  1. Используйте хотя бы одну виртуальную функцию, которая не встроена. Определение виртуального деструктора также хорошо, поскольку это не встроенная функция.

  2. Отключите предупреждение, как показано ниже.

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

  3. Используйте только файлы .h для объявлений классов.