clang: нет внешних определений виртуальных методов (чистый абстрактный класс C++)
Я пытаюсь скомпилировать следующий простой код C++, используя 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 есть внешнее определение. Что мне здесь не хватает?
редактировать
Я не думаю, что этот вопрос является дубликатом: в чем смысл -Wweak-vtables clang? как предложил Филипп Розен. В моем вопросе я специально ссылаюсь на чистые абстрактные классы (не упомянутые в предложенном дубликате). я знаю как -Wweak-vtables
работает с неабстрактными классами, и я в порядке с этим. В моем примере я определяю деструктор (который является чисто абстрактным) в файле реализации. Это должно предотвратить появление ошибок Clang даже при -Wweak-vtables
,
3 ответа
Мы не хотим размещать vtable в каждом модуле перевода. Таким образом, должно быть некоторое упорядочение блоков перевода, так что мы можем сказать, что мы помещаем vtable в "первую" единицу перевода. Если этот порядок не определен, мы выдаем предупреждение.
Вы найдете ответ в Itanium CXX ABI. В разделе о виртуальных таблицах (5.2.3) вы найдете:
Виртуальная таблица для класса создается в том же объекте, содержащем определение его ключевой функции, то есть первой не чистой виртуальной функции, которая не является встроенной в точке определения класса. Если ключевой функции нет, она испускается повсеместно. Излучаемая виртуальная таблица включает в себя полную группу виртуальных таблиц для класса, любые виртуальные таблицы новой конструкции, необходимые для подобъектов, и VTT для класса. Они генерируются в группе COMDAT, с искаженным именем виртуальной таблицы в качестве идентифицирующего символа. Обратите внимание, что если ключевая функция не объявлена как встроенная в определении класса, но ее определение позже всегда объявляется как встроенное, она будет передаваться в каждом объекте, содержащем определение.
ПРИМЕЧАНИЕ. В реферате в качестве ключевой функции может использоваться чистый виртуальный деструктор, так как он должен быть определен, даже если он чистый. Однако комитет ABI не осознавал этого факта до тех пор, пока не была завершена спецификация ключевой функции; поэтому чистый виртуальный деструктор не может быть ключевой функцией.
Второй раздел - это ответ на ваш вопрос. Чистый виртуальный деструктор не является ключевой функцией. Поэтому неясно, где разместить виртуальную таблицу и она размещена везде. Как следствие мы получаем предупреждение.
Вы даже найдете это объяснение в исходной документации 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 во всех модулях перевода, которые включают объявление полиморфного класса.
Когда компилятор видит объявление класса с виртуальными функциями, он проверяет, существуют ли виртуальные функции, которые только объявлены, но не определены внутри объявления класса. Если есть только одна такая функция, компилятор точно знает, что она должна быть где-то определена (в противном случае программа не будет ссылаться), и выдает vtable только в модуль перевода, содержащий определение этой функции. Если таких функций несколько, компилятор выбирает одну из них, используя некоторые детерминированные критерии выбора, и - в отношении решения о том, куда выводить vtable - игнорирует другие. Самый простой способ выбрать такую единственную репрезентативную виртуальную функцию - это взять первую из набора кандидатов, и это то, что делает Clang.
Таким образом, ключом к этой оптимизации является выбор виртуального метода таким образом, чтобы компилятор мог гарантировать, что он встретит (одно) определение этого метода в некоторой единице перевода.
Теперь, что если объявление класса содержит чисто виртуальные функции? Программист может обеспечить реализацию чисто виртуальной функции, но он (а) не обязан! Поэтому чисто виртуальные функции не входят в список возможных виртуальных методов, из которых компилятор может выбрать репрезентативный.
Но есть одно исключение - чистый виртуальный деструктор!
Чистый виртуальный деструктор - это особый случай:
- Абстрактный класс не имеет смысла, если вы не собираетесь выводить из него другие классы.
- Деструктор подкласса всегда вызывает деструктор базового класса.
- Деструктор класса, производного от класса с виртуальным деструктором, автоматически становится виртуальной функцией.
- Все виртуальные функции всех классов, из которых программа создает объекты, обычно связаны в конечный исполняемый файл (включая виртуальные функции, которые могут быть статически доказаны как неиспользуемые, хотя для этого потребуется статический анализ всей программы).
- Поэтому чистый виртуальный деструктор должен иметь пользовательское определение.
Таким образом, предупреждение clang в примере вопроса концептуально не оправдано.
Однако с практической точки зрения важность этого примера минимальна, так как чистый виртуальный деструктор редко, если вообще необходим. Я не могу представить более или менее реалистичный случай, когда чистый виртуальный деструктор не будет сопровождаться другой чистой виртуальной функцией. Но в такой ситуации необходимость в чистоте (виртуального) деструктора полностью исчезает, поскольку класс становится абстрактным из-за присутствия других чистых виртуальных методов.
В итоге я реализовал тривиальный виртуальный деструктор, а не оставил его чисто виртуальным.
Так что вместо
class A {
public:
virtual ~A() = 0;
};
я использую
class A {
public:
virtual ~A();
};
Затем реализуйте тривиальный деструктор в файле.cpp:
A::~A()
{}
Это эффективно прикрепляет vtable к файлу.cpp вместо вывода его в нескольких единицах перевода (объектах) и успешно избегает предупреждения -Wweak-vtables.
Это можно решить тремя способами.
Используйте хотя бы одну виртуальную функцию, которая не является встроенной. Также можно определить виртуальный деструктор, поскольку это не встроенная функция.
Отключите предупреждение, как показано ниже.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wweak-vtables"
class ClassName : public Parent
{
...
};
#pragma clang diagnostic pop
- Используйте только файлы.h для объявлений классов.