Когда НЕ использовать виртуальные деструкторы?


48

Я полагал, что много раз искал о виртуальных деструкторах, большинство упоминало назначение виртуальных деструкторов и почему вам нужны виртуальные деструкторы. Также я думаю, что в большинстве случаев деструкторы должны быть виртуальными.

Тогда возникает вопрос: почему с ++ не устанавливает все виртуальные деструкторы по умолчанию? или в других вопросах:

Когда мне НЕ нужно использовать виртуальные деструкторы?

В каком случае я не должен использовать виртуальные деструкторы?

Какова стоимость использования виртуальных деструкторов, если я использую их, даже если они не нужны?


6
А что если ваш класс не должен наследоваться? Посмотрите на многие стандартные классы библиотек, немногие имеют виртуальные функции, потому что они не предназначены для наследования.
Какой-то программист чувак

4
Также я думаю, что в большинстве случаев деструкторы должны быть виртуальными. Нет. Не за что. Так думают только те, кто злоупотребляет наследованием (а не в пользу композиции). Я видел целые приложения только с несколькими базовыми классами и виртуальными функциями.
Матье М.

1
@underscore_d В типичных реализациях для любого полиморфного класса генерировался бы дополнительный код, если бы все такие неявные вещи не были девиртуализированы и оптимизированы. В обычных ABI это включает в себя как минимум один vtable для каждого класса. Расположение класса также должно быть изменено. Вы не сможете вернуться надежно после того, как вы опубликовали такой класс как части какого-либо общедоступного интерфейса, потому что его повторное изменение нарушит совместимость ABI, поскольку явно (если вообще возможно) ожидать девиртуализации в качестве контрактов интерфейса в целом.
FrankHB

1
@underscore_d Фраза «во время компиляции» является неточной, но я думаю, что это означает, что виртуальный деструктор не может быть тривиальным, ни с заданным constexpr, поэтому лишнюю генерацию кода трудно избежать (если вы полностью не избегаете уничтожения таких объектов), поэтому это более или менее повредит производительности во время выполнения.
FrankHB

2
@underscore_d «Указатель» кажется красной селедкой. Возможно, это должен быть указатель на член (который по определению не является указателем). В обычных ABI указатель на член часто не помещается в машинное слово (как типичные указатели), и изменение класса с неполиморфного на полиморфный часто приводит к изменению размера указателя на член этого класса.
FrankHB

Ответы:


41

Если вы добавите виртуальный деструктор в класс:

  • в большинстве (всех?) текущих реализаций C ++ каждый экземпляр объекта этого класса должен хранить указатель на таблицу виртуальной диспетчеризации для типа времени выполнения, и эта таблица виртуальной диспетчеризации сама добавляется в исполняемый образ

  • адрес таблицы виртуальной диспетчеризации не обязательно действителен для разных процессов, что может помешать безопасному совместному использованию таких объектов в общей памяти

  • иметь встроенный виртуальный указатель, который мешает созданию класса с макетом памяти, соответствующим некоторому известному формату ввода или вывода (например, так что он Price_Tick*может быть нацелен непосредственно на соответствующим образом выровненную память во входящем пакете UDP и использоваться для анализа / доступа или изменения данных, или размещение newтакого класса для записи данных в исходящий пакет)

  • Сами вызовы деструктора могут - при определенных условиях - быть отправлены виртуально и, следовательно, вне линии, тогда как не виртуальные деструкторы могут быть встроенными или оптимизированными, если они тривиальны или не имеют отношения к вызывающей стороне.

Аргумент «не предназначен для наследования от» не был бы практической причиной не всегда наличия виртуального деструктора, если бы он также не был на практике хуже, как объяснено выше; но, учитывая, что это хуже, это главный критерий, когда нужно платить стоимость: по умолчанию виртуальный деструктор используется по умолчанию , если ваш класс предназначен для использования в качестве базового класса . Это не всегда необходимо, но гарантирует, что классы в иерархии могут использоваться более свободно без случайного неопределенного поведения, если деструктор производного класса вызывается с использованием указателя или ссылки на базовый класс.

«В большинстве случаев деструкторы должны быть виртуальными»

Не так ... у многих классов такой необходимости нет. Существует так много примеров, когда ненужно перечислять их, но глупо перечислять их, но просто просмотрите вашу стандартную библиотеку или произнесите повышение, и вы увидите, что в подавляющем большинстве классов нет виртуальных деструкторов. В бусте 1,53 я считаю 72 виртуальных деструктора из 494.


23

В каком случае я не должен использовать виртуальные деструкторы?

  1. Для конкретного класса, который не хочет наследоваться.
  2. Для базового класса без полиморфного удаления. Либо клиенты не должны иметь возможность полиморфно удалять указатель на Base.

КСТАТИ,

В каком случае следует использовать виртуальные деструкторы?

Для базовых классов с полиморфным удалением.


7
+1 за # 2, особенно без полиморфного удаления . Если ваш деструктор никогда не может быть вызван с помощью базового указателя, делать его виртуальным не нужно и не нужно, особенно если ваш класс ранее не был виртуальным (поэтому он становится вновь раздутым с помощью RTTI). Чтобы защититься от любого пользователя, нарушающего это, как советовал Херб Саттер, вы должны сделать dtor базового класса защищенным и не виртуальным, чтобы он мог вызываться только / после производного деструктора.
underscore_d

@underscore_d IMHO , что важный момент , который я пропустил в ответах, как и в присутствии наследования единственный случай , когда я не нужен виртуальный конструктор, когда я могу убедиться , что она не нужна
formerlyknownas_463035818

14

Какова стоимость использования виртуальных деструкторов, если я использую их, даже если они не нужны?

Стоимость введения любой виртуальной функции в класс (унаследованная или часть определения класса) является, возможно, очень высокой (или не зависящей от объекта) начальной стоимостью виртуального указателя, хранящегося на объект, например:

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 ++ склонен неявно акцентировать внимание (поскольку конструкции имеют тенденцию становиться действительно хрупкими и неуклюжими и, возможно, даже небезопасными в противном случае), заключается в том, что наследование не является механизмом, предназначенным для использования в качестве запоздалой мысли. Это механизм расширяемости с учетом полиморфизма, но он требует предвидения того, где требуется расширяемость. В результате ваши базовые классы должны быть спроектированы как корни иерархии наследования заранее, а не как что-то, от чего вы наследуете позже как запоздалая мысль без какого-либо предвидения заранее.

В тех случаях, когда вы просто хотите наследовать для повторного использования существующего кода, составление часто настоятельно рекомендуется (принцип составного повторного использования).


9

Почему c ++ не устанавливает все виртуальные деструкторы по умолчанию? Стоимость дополнительного хранилища и вызова таблицы виртуальных методов. C ++ используется для системного программирования с малым временем ожидания, где это может быть обременительным.


Деструкторы не должны использоваться в первую очередь в жестких системах реального времени, поскольку многие ресурсы, такие как динамическая память, не могут быть использованы для обеспечения надежных сроков
Марко А.

9
@MarcoA. С каких пор деструкторы подразумевают динамическое распределение памяти?
chbaker0

@ chbaker0 Я использовал «лайк». Они просто не используются в моем опыте.
Марко А.

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

@msalters, который заставляет меня задуматься: представьте себе программу, в которой стоимость каждой операции хранилась в системе типов. Разрешение проверки во время компиляции гарантий в реальном времени.
Якк

5

Это хороший пример того, когда не следует использовать виртуальный деструктор. От Скотта Мейерса:

Если класс не содержит никаких виртуальных функций, это часто указывает на то, что он не предназначен для использования в качестве базового класса. Когда класс не предназначен для использования в качестве базового класса, создание виртуального деструктора обычно является плохой идеей. Рассмотрим этот пример, основанный на обсуждении в ARM:

// class for representing 2D points
class Point {
public:
    Point(short int xCoord, short int yCoord);
    ~Point();
private:
    short int x, y;
};

Если short int занимает 16 бит, объект Point может помещаться в 32-битный регистр. Кроме того, объект Point может быть передан в виде 32-битной величины функциям, написанным на других языках, таких как C или FORTRAN. Если деструктор Point становится виртуальным, ситуация меняется.

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


If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class.Wut. Кто-нибудь еще помнит старые добрые времена, когда нам было позволено использовать классы и наследование для создания последовательных слоев многократно используемых элементов и поведения, не заботясь вообще о виртуальных методах? Да ладно, Скотт. Я понимаю суть, но это «часто» действительно достигает.
underscore_d

3

Виртуальный деструктор добавляет стоимость времени выполнения. Стоимость особенно велика, если у класса нет других виртуальных методов. Виртуальный деструктор также необходим только в одном конкретном сценарии, когда объект удаляется или иным образом уничтожается с помощью указателя на базовый класс. В этом случае деструктор базового класса должен быть виртуальным, а деструктор любого производного класса будет неявно виртуальным. Есть несколько сценариев, где полиморфный базовый класс используется таким образом, что деструктор не должен быть виртуальным:

  • Если экземпляры производных классов не размещены в куче, например, только непосредственно в стеке или внутри других объектов. (За исключением случаев, когда вы используете неинициализированную память и оператор размещения new.)
  • Если экземпляры производных классов размещаются в куче, но удаление происходит только через указатели на самый производный класс, например, существует std::unique_ptr<Derived>, и полиморфизм происходит только через не принадлежащие указатели и ссылки. Другой пример - когда объекты размещаются с использованием std::make_shared<Derived>(). Это хорошо для использования, std::shared_ptr<Base>а также, если исходный указатель был std::shared_ptr<Derived>. Это связано с тем, что совместно используемые указатели имеют собственную динамическую диспетчеризацию для деструкторов (средства удаления), которые не обязательно зависят от виртуального деструктора базового класса.

Конечно, любое соглашение об использовании объектов только вышеупомянутыми способами может быть легко нарушено. Поэтому совет Херба Саттера остается в силе как никогда: «Деструкторы базового класса должны быть либо публичными и виртуальными, либо защищенными и не виртуальными». Таким образом, если кто-то попытается удалить указатель на базовый класс с не виртуальным деструктором, он (а), скорее всего, получит ошибку нарушения доступа во время компиляции.

Опять же, есть классы, которые не предназначены для (публичных) базовых классов. Моя личная рекомендация - сделать их finalна C ++ 11 или выше. Если он выполнен в виде квадратного колышка, скорее всего, он не будет работать очень хорошо, как круглый колышек. Это связано с моим предпочтением иметь явный договор наследования между базовым классом и производным классом, для шаблона проектирования NVI (не виртуального интерфейса), для абстрактных, а не конкретных базовых классов, и с моим отвращением к защищенным переменным-членам, среди прочего Но я знаю, что все эти взгляды в некоторой степени противоречивы.


1

Объявление деструктора virtualнеобходимо только тогда, когда вы планируете сделать его classнаследуемым. Обычно классы стандартной библиотеки (например, std::string) не предоставляют виртуальный деструктор и, следовательно, не предназначены для создания подклассов.


3
Причина в подклассе + использование полиморфизма. Виртуальный деструктор требуется только в том случае, если требуется динамическое разрешение, то есть ссылка / указатель / что-либо на мастер-класс может фактически ссылаться на экземпляр подкласса.
Мишель Бийо

2
@MichelBillaud на самом деле вы все еще можете иметь полиморфизм без виртуальных dtors. Виртуальный dtor требуется ТОЛЬКО для полиморфного удаления, то есть вызова deleteуказателя на базовый класс.
chbaker0

1

В конструкторе будут накладные расходы на создание виртуальной таблицы (если у вас нет других виртуальных функций, в этом случае вы, вероятно, но не всегда, должны иметь виртуальный деструктор). И если у вас нет никаких других виртуальных функций, это делает ваш объект на один размер указателя больше, чем необходимо. Очевидно, что увеличенный размер может оказать большое влияние на мелкие объекты.

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

У вас должен быть виртуальный деструктор, если ваш класс задуман как базовый класс, в частности, если он может быть создан / уничтожен каким-либо другим объектом, а не кодом, который знает, какого типа он находится при создании, тогда вам нужен виртуальный деструктор.

Если вы не уверены, используйте виртуальный деструктор. Легче удалить виртуальный, если он обнаруживается как проблема, чем пытаться найти ошибку, вызванную тем, что «правильный деструктор не вызван».

Короче говоря, у вас не должно быть виртуального деструктора, если: 1. У вас нет виртуальных функций. 2. Не наследуйте от класса (отметьте его finalв C ++ 11, таким образом компилятор скажет, пытаетесь ли вы наследовать от него).

В большинстве случаев создание и уничтожение не составляют основную часть времени, затрачиваемого на использование определенного объекта, если нет «большого количества контента» (создание строки размером 1 МБ, очевидно, займет некоторое время, потому что по крайней мере 1 МБ данных необходимо скопировать с того места, где оно находится в данный момент). Уничтожение строки размером 1 МБ ничем не хуже, чем уничтожение строки размером 150 В, и то и другое потребует освобождения хранилища строк и не намного больше, поэтому время, потраченное там, обычно одинаково [если только это не отладочная сборка, где освобождение часто заполняет память «шаблон отравления» - но это не то, как вы собираетесь запускать ваше реальное приложение в производстве].

Короче говоря, есть небольшие накладные расходы, но для небольших объектов это может иметь значение.

Также обратите внимание, что компиляторы могут оптимизировать виртуальный поиск в некоторых случаях, так что это только штраф

Как всегда, когда дело доходит до производительности, объема памяти и тому подобного: сравнительный анализ, профилирование и измерение, сравнение результатов с альтернативами и поиск того, на что тратится МОСТ времени / памяти, и не пытайтесь оптимизировать 90% код, который не выполняется много [большинство приложений имеют около 10% кода, который сильно влияет на время выполнения, и 90% кода, который не имеет большого влияния вообще]. Делайте это на высоком уровне оптимизации, так что вы уже получите преимущество от того, что компилятор делает хорошую работу! И повтори, проверь еще раз и пошагово улучшай. Не пытайтесь быть умным и пытайтесь понять, что важно, а что нет, если у вас нет большого опыта работы с этим конкретным типом приложений.


1
«это будет непроизводительная нагрузка в конструкторе для создания vtable» - vtable обычно «создается» компилятором для каждого класса, при этом конструктор имеет только накладные расходы на хранение указателя на него в создаваемом экземпляре объекта.
Тони

Кроме того ... Я все об избежании преждевременной оптимизации, но, наоборот, You **should** have a virtual destructor if your class is intended as a base-classэто грубое упрощение и преждевременная пессимизация . Это необходимо только в том случае, если кому-либо разрешено удалять производный класс через указатель на базу. Во многих ситуациях это не так. Если вы знаете, что это так, то конечно, понести накладные расходы. Который, между прочим, всегда добавляется, даже если фактические вызовы могут быть статически разрешены компилятором. Иначе, когда вы должным образом контролируете, что люди могут делать с вашими объектами, это не имеет смысла
underscore_d
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.