Я полагал, что много раз искал о виртуальных деструкторах, большинство упоминало назначение виртуальных деструкторов и почему вам нужны виртуальные деструкторы. Также я думаю, что в большинстве случаев деструкторы должны быть виртуальными.
Тогда возникает вопрос: почему с ++ не устанавливает все виртуальные деструкторы по умолчанию? или в других вопросах:
Когда мне НЕ нужно использовать виртуальные деструкторы?
В каком случае я не должен использовать виртуальные деструкторы?
Какова стоимость использования виртуальных деструкторов, если я использую их, даже если они не нужны?
c++
virtual-functions
ggrr
источник
источник
Ответы:
Если вы добавите виртуальный деструктор в класс:
в большинстве (всех?) текущих реализаций C ++ каждый экземпляр объекта этого класса должен хранить указатель на таблицу виртуальной диспетчеризации для типа времени выполнения, и эта таблица виртуальной диспетчеризации сама добавляется в исполняемый образ
адрес таблицы виртуальной диспетчеризации не обязательно действителен для разных процессов, что может помешать безопасному совместному использованию таких объектов в общей памяти
иметь встроенный виртуальный указатель, который мешает созданию класса с макетом памяти, соответствующим некоторому известному формату ввода или вывода (например, так что он
Price_Tick*
может быть нацелен непосредственно на соответствующим образом выровненную память во входящем пакете UDP и использоваться для анализа / доступа или изменения данных, или размещениеnew
такого класса для записи данных в исходящий пакет)Сами вызовы деструктора могут - при определенных условиях - быть отправлены виртуально и, следовательно, вне линии, тогда как не виртуальные деструкторы могут быть встроенными или оптимизированными, если они тривиальны или не имеют отношения к вызывающей стороне.
Аргумент «не предназначен для наследования от» не был бы практической причиной не всегда наличия виртуального деструктора, если бы он также не был на практике хуже, как объяснено выше; но, учитывая, что это хуже, это главный критерий, когда нужно платить стоимость: по умолчанию виртуальный деструктор используется по умолчанию , если ваш класс предназначен для использования в качестве базового класса . Это не всегда необходимо, но гарантирует, что классы в иерархии могут использоваться более свободно без случайного неопределенного поведения, если деструктор производного класса вызывается с использованием указателя или ссылки на базовый класс.
Не так ... у многих классов такой необходимости нет. Существует так много примеров, когда ненужно перечислять их, но глупо перечислять их, но просто просмотрите вашу стандартную библиотеку или произнесите повышение, и вы увидите, что в подавляющем большинстве классов нет виртуальных деструкторов. В бусте 1,53 я считаю 72 виртуальных деструктора из 494.
источник
КСТАТИ,
Для базовых классов с полиморфным удалением.
источник
Стоимость введения любой виртуальной функции в класс (унаследованная или часть определения класса) является, возможно, очень высокой (или не зависящей от объекта) начальной стоимостью виртуального указателя, хранящегося на объект, например:
В этом случае стоимость памяти относительно огромна. Фактический объем памяти экземпляра класса теперь часто будет выглядеть так на 64-битных архитектурах:
Всего для этого
Integer
класса 16 байтов, а не 4 байта. Если мы сохраним миллион из них в массиве, мы получим 16 мегабайт памяти: в два раза больше типичного 8 МБ кэш-памяти L3 и многократное повторение такого массива может быть во много раз медленнее, чем эквивалент в 4 мегабайта. без виртуального указателя в результате дополнительных пропусков кэша и ошибок страницы.Однако стоимость виртуального указателя на объект не увеличивается с увеличением количества виртуальных функций. Вы можете иметь 100 виртуальных функций-членов в классе, и издержки на экземпляр будут по-прежнему одним виртуальным указателем.
Виртуальный указатель обычно является более насущной проблемой с точки зрения накладных расходов. Тем не менее, в дополнение к виртуальному указателю для каждого экземпляра существует стоимость для каждого класса. Каждый класс с виртуальными функциями генерирует
vtable
в памяти память, в которой хранятся адреса тех функций, которые он должен фактически вызывать (виртуальная / динамическая диспетчеризация) при выполнении вызова виртуальной функции.vptr
Хранится на экземпляр , то указывает на этот класс-специфическихvtable
. Эти издержки, как правило, представляют меньшую проблему, но они могут привести к завышению размера вашего двоичного файла и добавить немного затрат времени выполнения, если эти накладные расходы были оплачены без необходимости за тысячу классов в сложной кодовой базе, например. Этаvtable
сторона стоимости действительно увеличивается пропорционально с увеличением и больше виртуальных функций в миксе.Разработчики Java, работающие в областях, критичных к производительности, очень хорошо понимают этот тип издержек (хотя часто описываются в контексте бокса), поскольку пользовательский тип Java неявно наследуется из центрального
object
базового класса, а все функции в Java неявно виртуальны (переопределяемы). ) в природе, если не указано иное. В результате JavaInteger
также имеет тенденцию требовать 16 байт памяти на 64-битных платформах в результатеvptr
метаданных этого стиля, связанных с экземпляром, и в Java обычно невозможно обернуть что-то вроде одногоint
в класс без оплаты времени выполнения. стоимость исполнения для него.C ++ действительно способствует повышению производительности благодаря типу мышления «плати, как есть», а также множеству аппаратных проектов, унаследованных от С. каждый участвующий класс / экземпляр. Если производительность не является одной из ключевых причин, по которой вы используете такой язык, как C ++, вы могли бы получить больше пользы от других языков программирования, поскольку большая часть языка C ++ менее безопасна и более сложна, чем в идеале, когда производительность часто снижается. главная причина, чтобы поддержать такой дизайн.
Частенько. Если класс не предназначен для наследования, ему не нужен виртуальный деструктор, и он в конечном итоге заплатит, возможно, большие накладные расходы за то, что ему не нужно. Аналогично, даже если класс предназначен для наследования, но вы никогда не удаляете экземпляры подтипов через базовый указатель, для него также не требуется виртуальный деструктор. В этом случае безопасной практикой является определение защищенного невиртуального деструктора, например, так:
На самом деле легче охватить, когда вы должны использовать виртуальные деструкторы. Довольно часто гораздо больше классов в вашей кодовой базе не будут предназначены для наследования.
std::vector
Например, он не предназначен для наследования и, как правило, не должен наследоваться (очень шаткий дизайн), поскольку в этом случае он будет подвержен этой проблеме удаления базовых указателей (std::vector
намеренно избегает виртуального деструктора) в дополнение к неуклюжим проблемам нарезки объектов, если ваш Производный класс добавляет любое новое состояние.В общем случае унаследованный класс должен иметь открытый виртуальный деструктор или защищенный не виртуальный. Из
C++ Coding Standards
главы 50:Одна из вещей, на которую C ++ склонен неявно акцентировать внимание (поскольку конструкции имеют тенденцию становиться действительно хрупкими и неуклюжими и, возможно, даже небезопасными в противном случае), заключается в том, что наследование не является механизмом, предназначенным для использования в качестве запоздалой мысли. Это механизм расширяемости с учетом полиморфизма, но он требует предвидения того, где требуется расширяемость. В результате ваши базовые классы должны быть спроектированы как корни иерархии наследования заранее, а не как что-то, от чего вы наследуете позже как запоздалая мысль без какого-либо предвидения заранее.
В тех случаях, когда вы просто хотите наследовать для повторного использования существующего кода, составление часто настоятельно рекомендуется (принцип составного повторного использования).
источник
Почему c ++ не устанавливает все виртуальные деструкторы по умолчанию? Стоимость дополнительного хранилища и вызова таблицы виртуальных методов. C ++ используется для системного программирования с малым временем ожидания, где это может быть обременительным.
источник
Это хороший пример того, когда не следует использовать виртуальный деструктор. От Скотта Мейерса:
Если класс не содержит никаких виртуальных функций, это часто указывает на то, что он не предназначен для использования в качестве базового класса. Когда класс не предназначен для использования в качестве базового класса, создание виртуального деструктора обычно является плохой идеей. Рассмотрим этот пример, основанный на обсуждении в ARM:
Если 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. Кто-нибудь еще помнит старые добрые времена, когда нам было позволено использовать классы и наследование для создания последовательных слоев многократно используемых элементов и поведения, не заботясь вообще о виртуальных методах? Да ладно, Скотт. Я понимаю суть, но это «часто» действительно достигает.Виртуальный деструктор добавляет стоимость времени выполнения. Стоимость особенно велика, если у класса нет других виртуальных методов. Виртуальный деструктор также необходим только в одном конкретном сценарии, когда объект удаляется или иным образом уничтожается с помощью указателя на базовый класс. В этом случае деструктор базового класса должен быть виртуальным, а деструктор любого производного класса будет неявно виртуальным. Есть несколько сценариев, где полиморфный базовый класс используется таким образом, что деструктор не должен быть виртуальным:
std::unique_ptr<Derived>
, и полиморфизм происходит только через не принадлежащие указатели и ссылки. Другой пример - когда объекты размещаются с использованиемstd::make_shared<Derived>()
. Это хорошо для использования,std::shared_ptr<Base>
а также, если исходный указатель былstd::shared_ptr<Derived>
. Это связано с тем, что совместно используемые указатели имеют собственную динамическую диспетчеризацию для деструкторов (средства удаления), которые не обязательно зависят от виртуального деструктора базового класса.Конечно, любое соглашение об использовании объектов только вышеупомянутыми способами может быть легко нарушено. Поэтому совет Херба Саттера остается в силе как никогда: «Деструкторы базового класса должны быть либо публичными и виртуальными, либо защищенными и не виртуальными». Таким образом, если кто-то попытается удалить указатель на базовый класс с не виртуальным деструктором, он (а), скорее всего, получит ошибку нарушения доступа во время компиляции.
Опять же, есть классы, которые не предназначены для (публичных) базовых классов. Моя личная рекомендация - сделать их
final
на C ++ 11 или выше. Если он выполнен в виде квадратного колышка, скорее всего, он не будет работать очень хорошо, как круглый колышек. Это связано с моим предпочтением иметь явный договор наследования между базовым классом и производным классом, для шаблона проектирования NVI (не виртуального интерфейса), для абстрактных, а не конкретных базовых классов, и с моим отвращением к защищенным переменным-членам, среди прочего Но я знаю, что все эти взгляды в некоторой степени противоречивы.источник
Объявление деструктора
virtual
необходимо только тогда, когда вы планируете сделать егоclass
наследуемым. Обычно классы стандартной библиотеки (например,std::string
) не предоставляют виртуальный деструктор и, следовательно, не предназначены для создания подклассов.источник
delete
указателя на базовый класс.В конструкторе будут накладные расходы на создание виртуальной таблицы (если у вас нет других виртуальных функций, в этом случае вы, вероятно, но не всегда, должны иметь виртуальный деструктор). И если у вас нет никаких других виртуальных функций, это делает ваш объект на один размер указателя больше, чем необходимо. Очевидно, что увеличенный размер может оказать большое влияние на мелкие объекты.
Для получения vtable требуется дополнительное чтение памяти, а затем через него вызывается косвенная функция, которая при вызове деструктора накладывается на не виртуальный деструктор. И, конечно, как следствие, немного дополнительного кода, генерируемого для каждого вызова деструктора. Это для случаев, когда компилятор не может вывести фактический тип - в тех случаях, когда он может вывести фактический тип, компилятор не будет использовать vtable, но вызовет деструктор напрямую.
У вас должен быть виртуальный деструктор, если ваш класс задуман как базовый класс, в частности, если он может быть создан / уничтожен каким-либо другим объектом, а не кодом, который знает, какого типа он находится при создании, тогда вам нужен виртуальный деструктор.
Если вы не уверены, используйте виртуальный деструктор. Легче удалить виртуальный, если он обнаруживается как проблема, чем пытаться найти ошибку, вызванную тем, что «правильный деструктор не вызван».
Короче говоря, у вас не должно быть виртуального деструктора, если: 1. У вас нет виртуальных функций. 2. Не наследуйте от класса (отметьте его
final
в C ++ 11, таким образом компилятор скажет, пытаетесь ли вы наследовать от него).В большинстве случаев создание и уничтожение не составляют основную часть времени, затрачиваемого на использование определенного объекта, если нет «большого количества контента» (создание строки размером 1 МБ, очевидно, займет некоторое время, потому что по крайней мере 1 МБ данных необходимо скопировать с того места, где оно находится в данный момент). Уничтожение строки размером 1 МБ ничем не хуже, чем уничтожение строки размером 150 В, и то и другое потребует освобождения хранилища строк и не намного больше, поэтому время, потраченное там, обычно одинаково [если только это не отладочная сборка, где освобождение часто заполняет память «шаблон отравления» - но это не то, как вы собираетесь запускать ваше реальное приложение в производстве].
Короче говоря, есть небольшие накладные расходы, но для небольших объектов это может иметь значение.
Также обратите внимание, что компиляторы могут оптимизировать виртуальный поиск в некоторых случаях, так что это только штраф
Как всегда, когда дело доходит до производительности, объема памяти и тому подобного: сравнительный анализ, профилирование и измерение, сравнение результатов с альтернативами и поиск того, на что тратится МОСТ времени / памяти, и не пытайтесь оптимизировать 90% код, который не выполняется много [большинство приложений имеют около 10% кода, который сильно влияет на время выполнения, и 90% кода, который не имеет большого влияния вообще]. Делайте это на высоком уровне оптимизации, так что вы уже получите преимущество от того, что компилятор делает хорошую работу! И повтори, проверь еще раз и пошагово улучшай. Не пытайтесь быть умным и пытайтесь понять, что важно, а что нет, если у вас нет большого опыта работы с этим конкретным типом приложений.
источник
You **should** have a virtual destructor if your class is intended as a base-class
это грубое упрощение и преждевременная пессимизация . Это необходимо только в том случае, если кому-либо разрешено удалять производный класс через указатель на базу. Во многих ситуациях это не так. Если вы знаете, что это так, то конечно, понести накладные расходы. Который, между прочим, всегда добавляется, даже если фактические вызовы могут быть статически разрешены компилятором. Иначе, когда вы должным образом контролируете, что люди могут делать с вашими объектами, это не имеет смысла