Стоит отметить, что это связано с оптимизацией под названием «идентичное свертывание COMDAT» или ICF. Это функция компоновщика, в которой все идентичные функции (т. Е. Пустые функции) объединены в одну.
Не каждый компоновщик поддерживает это, и не каждый компоновщик желает это делать (поскольку язык говорит, что разные функции требуют другой адрес), но ваш набор инструментов может иметь это. Это было бы быстро и легко.
Я собираюсь предположить, что ваша проблема воспроизводится на этом игрушечном примере :
#include <iostream>
#include <memory>
#include <variant>
extern unsigned nondet();
struct Base {
virtual const char* what() const = 0;
virtual ~Base() = default;
};
struct A final : Base {
const char* what() const override {
return "a";
}
};
struct B final : Base {
const char* what() const override {
return "b";
}
};
std::unique_ptr<Base> parse(unsigned v) {
if (v == 0) {
return std::make_unique<A>();
} else if (v == 1) {
return std::make_unique<B>();
} else {
__builtin_unreachable();
}
}
const char* what(const Base& b) {
return b.what(); // virtual dispatch
}
const char* what(const std::unique_ptr<Base>& b) {
return what(*b);
}
int main() {
unsigned v = nondet();
auto packet = parse(v);
std::cout << what(packet) << std::endl;
}
Разборка показывает, что A::~A
и B::~B
имеют (несколько) списков, даже если они пусты и идентичны. Это с = default
и final
.
Если удаляется virtual
, тогда эти пустые определения go удаляются, и мы достигаем цели - но теперь, когда unique_ptr удаляет объект, мы вызываем undefined поведение.
У нас есть три варианта оставить деструктор не виртуальным при сохранении четко определенного поведения, два из которых полезны, а другой - нет.
Бесполезно: Первый вариант - использовать shared_ptr
. Это работает, потому что shared_ptr
на самом деле печатает свою функцию удаления (см. этот вопрос ), поэтому ни в коем случае не удаляет через базу. Другими словами, когда вы делаете shared_ptr<T>(u)
для некоторого u
, полученного из T
, shared_ptr
сохраняет указатель функции непосредственно на U::~U
.
Однако это стирание типа просто повторно вводит проблема и генерирует еще больше пустых виртуальных деструкторов. См. пример модифицированной игрушки для сравнения. Я упомяну это для полноты, на случай, если вы уже поместили их в shared_ptr на стороне.
Полезно: альтернатива - избежать виртуальной отправки для управления временем жизни и использовать variant
. Не совсем правильно делать такое общее утверждение, но , как правило, , вы можете добиться меньшего кода и даже некоторого ускорения с диспетчеризацией тегов, так как вы избегаете указания vtables и динамического c размещения.
Это требует самого большого изменения в вашем коде, потому что объект, представляющий ваш пакет, должен взаимодействовать по-другому (это больше не отношение is-a):
#include <iostream>
#include <boost/variant.hpp>
extern unsigned nondet();
struct Base {
~Base() = default;
};
struct A final : Base {
const char* what() const {
return "a";
}
};
struct B final : Base {
const char* what() const {
return "b";
}
};
typedef boost::variant<A, B> packet_t;
packet_t parse(unsigned v) {
if (v == 0) {
return A();
} else if (v == 1) {
return B();
} else {
__builtin_unreachable();
}
}
const char* what(const packet_t& p) {
return boost::apply_visitor([](const auto& v){
return v.what();
}, p);
}
int main() {
unsigned v = nondet();
auto packet = parse(v);
std::cout << what(packet) << std::endl;
}
Я использовал Boost.Variant, потому что он выдает самый маленький код . Досадно, что std::variant
настаивает на создании некоторых незначительных, но существующих vtables для реализации самого себя - я чувствую, что это немного противоречит цели, хотя даже с вариантом vtables код остается намного меньше в целом.
Я хочу указать хороший результат современных оптимизирующих компиляторов. Обратите внимание на итоговую реализацию what
:
what(boost::variant<A, B> const&):
mov eax, DWORD PTR [rdi]
cdq
cmp eax, edx
mov edx, OFFSET FLAT:.LC1
mov eax, OFFSET FLAT:.LC0
cmove rax, rdx
ret
Компилятор понимает закрытый набор опций в варианте, лямбда-типирование утки доказало, что каждая опция действительно имеет функцию-член ...::what
, и поэтому на самом деле это просто выбор строкового литерала для возврата на основе значения варианта.
Компромисс с вариантом заключается в том, что у вас должен быть закрытый набор опций, и у вас больше нет виртуального интерфейса, обеспечивающего существование определенных функций. В ответ вы получаете меньший код, и компилятор часто может видеть сквозь «стену».
Однако, если мы определяем эти простые вспомогательные функции посетителя для «ожидаемой» функции-члена, он действует как средство проверки интерфейса - плюс у вас уже есть шаблоны вспомогательных классов, чтобы все было в порядке.
Наконец, как расширение вышеприведенного: вы всегда можете поддерживать некоторые виртуальные функции в базовом классе. Это может предложить лучшее из обоих миров, если стоимость vtables приемлема для вас:
#include <iostream>
#include <boost/variant.hpp>
extern unsigned nondet();
struct Base {
virtual const char* what() const = 0;
~Base() = default;
};
struct A final : Base {
const char* what() const override {
return "a";
}
};
struct B final : Base {
const char* what() const override {
return "b";
}
};
typedef boost::variant<A, B> packet_t;
packet_t parse(unsigned v) {
if (v == 0) {
return A();
} else if (v == 1) {
return B();
} else {
__builtin_unreachable();
}
}
const Base& to_base(const packet_t& p) {
return *boost::apply_visitor([](const auto& v){
return static_cast<const Base*>(&v);
}, p);
}
const char* what(const Base& b) {
return b.what(); // virtual dispatch
}
const char* what(const packet_t& p) {
return what(to_base(p));
}
int main() {
unsigned v = nondet();
auto packet = parse(v);
std::cout << what(packet) << std::endl;
}
Это дает довольно компактный код .
Что мы имеем здесь является виртуальным базовым классом (но без виртуального деструктора, так как он не нужен) и функцией to_base
, которая может принять вариант и вернуть вам общий базовый интерфейс. (И в такой иерархии, как ваша, у вас может быть несколько таких для каждой базы.)
С общей базы вы можете выполнять виртуальную рассылку. Это иногда проще в управлении и быстрее в зависимости от рабочей нагрузки, а дополнительная свобода стоит только некоторых vtables. В этом примере я реализовал what
как сначала преобразование в базовый класс, а затем выполнил виртуальную диспетчеризацию для функции-члена what
.
Опять же, я хочу указать на определение визит, на этот раз в to_base
:
to_base(boost::variant<A, B> const&):
lea rax, [rdi+8]
ret
Компилятор понимает закрытый набор классов, все они наследуются от Base
, и поэтому вообще не должен проверять какой-либо тег типа варианта.
В приведенном выше примере я использовал Boost.Variant. Не каждый может или хочет использовать Boost, но принципы ответа по-прежнему применимы: сохранить объект и отследить, какой тип объекта хранится в целом числе. Когда пришло время что-то сделать, взгляните на целое число и перейдите в нужное место кода.
Реализация варианта - это совсем другой вопрос. :)