Какова стоимость использования виртуальных деструкторов, если я использую их, даже если они не нужны?
Стоимость введения любой виртуальной функции в класс (унаследованная или часть определения класса) является, возможно, очень высокой (или не зависящей от объекта) начальной стоимостью виртуального указателя, хранящегося на объект, например:
struct Integer
{
virtual ~Integer() {}
int value;
};
В этом случае стоимость памяти относительно огромна. Фактический объем памяти экземпляра класса теперь часто будет выглядеть так на 64-битных архитектурах:
struct Integer
{
// 8 byte vptr overhead
int value; // 4 bytes
// typically 4 more bytes of padding for alignment of vptr
};
Всего для этого Integer
класса 16 байтов, а не 4 байта. Если мы сохраним миллион из них в массиве, мы получим 16 мегабайт памяти: в два раза больше типичного 8 МБ кэш-памяти L3 и многократное повторение такого массива может быть во много раз медленнее, чем эквивалент в 4 мегабайта. без виртуального указателя в результате дополнительных пропусков кэша и ошибок страницы.
Однако стоимость виртуального указателя на объект не увеличивается с увеличением количества виртуальных функций. Вы можете иметь 100 виртуальных функций-членов в классе, и издержки на экземпляр будут по-прежнему одним виртуальным указателем.
Виртуальный указатель обычно является более насущной проблемой с точки зрения накладных расходов. Тем не менее, в дополнение к виртуальному указателю для каждого экземпляра существует стоимость для каждого класса. Каждый класс с виртуальными функциями генерирует vtable
в памяти память, в которой хранятся адреса тех функций, которые он должен фактически вызывать (виртуальная / динамическая диспетчеризация) при выполнении вызова виртуальной функции. vptr
Хранится на экземпляр , то указывает на этот класс-специфических vtable
. Эти издержки, как правило, представляют меньшую проблему, но они могут привести к завышению размера вашего двоичного файла и добавить немного затрат времени выполнения, если эти накладные расходы были оплачены без необходимости за тысячу классов в сложной кодовой базе, например. Эта vtable
сторона стоимости действительно увеличивается пропорционально с увеличением и больше виртуальных функций в миксе.
Разработчики Java, работающие в областях, критичных к производительности, очень хорошо понимают этот тип издержек (хотя часто описываются в контексте бокса), поскольку пользовательский тип Java неявно наследуется из центрального object
базового класса, а все функции в Java неявно виртуальны (переопределяемы). ) в природе, если не указано иное. В результате Java Integer
также имеет тенденцию требовать 16 байт памяти на 64-битных платформах в результате vptr
метаданных этого стиля, связанных с экземпляром, и в Java обычно невозможно обернуть что-то вроде одного int
в класс без оплаты времени выполнения. стоимость исполнения для него.
Тогда возникает вопрос: почему с ++ не устанавливает все виртуальные деструкторы по умолчанию?
C ++ действительно способствует повышению производительности благодаря типу мышления «плати, как есть», а также множеству аппаратных проектов, унаследованных от С. каждый участвующий класс / экземпляр. Если производительность не является одной из ключевых причин, по которой вы используете такой язык, как C ++, вы могли бы получить больше пользы от других языков программирования, поскольку большая часть языка C ++ менее безопасна и более сложна, чем в идеале, когда производительность часто снижается. главная причина, чтобы поддержать такой дизайн.
Когда мне НЕ нужно использовать виртуальные деструкторы?
Частенько. Если класс не предназначен для наследования, ему не нужен виртуальный деструктор, и он в конечном итоге заплатит, возможно, большие накладные расходы за то, что ему не нужно. Аналогично, даже если класс предназначен для наследования, но вы никогда не удаляете экземпляры подтипов через базовый указатель, для него также не требуется виртуальный деструктор. В этом случае безопасной практикой является определение защищенного невиртуального деструктора, например, так:
class BaseClass
{
protected:
// Disallow deleting/destroying subclass objects through `BaseClass*`.
~BaseClass() {}
};
В каком случае я не должен использовать виртуальные деструкторы?
На самом деле легче охватить, когда вы должны использовать виртуальные деструкторы. Довольно часто гораздо больше классов в вашей кодовой базе не будут предназначены для наследования.
std::vector
Например, он не предназначен для наследования и, как правило, не должен наследоваться (очень шаткий дизайн), поскольку в этом случае он будет подвержен этой проблеме удаления базовых указателей ( std::vector
намеренно избегает виртуального деструктора) в дополнение к неуклюжим проблемам нарезки объектов, если ваш Производный класс добавляет любое новое состояние.
В общем случае унаследованный класс должен иметь открытый виртуальный деструктор или защищенный не виртуальный. Из C++ Coding Standards
главы 50:
50. Сделайте деструкторы базового класса общедоступными и виртуальными или защищенными и невиртуальными. Удалять или не удалять; вот в чем вопрос: если нужно разрешить удаление через указатель на базу Base, то деструктор Base должен быть открытым и виртуальным. В противном случае он должен быть защищен и не виртуален.
Одна из вещей, на которую C ++ склонен неявно акцентировать внимание (поскольку конструкции имеют тенденцию становиться действительно хрупкими и неуклюжими и, возможно, даже небезопасными в противном случае), заключается в том, что наследование не является механизмом, предназначенным для использования в качестве запоздалой мысли. Это механизм расширяемости с учетом полиморфизма, но он требует предвидения того, где требуется расширяемость. В результате ваши базовые классы должны быть спроектированы как корни иерархии наследования заранее, а не как что-то, от чего вы наследуете позже как запоздалая мысль без какого-либо предвидения заранее.
В тех случаях, когда вы просто хотите наследовать для повторного использования существующего кода, составление часто настоятельно рекомендуется (принцип составного повторного использования).