Коли НЕ використовувати віртуальних деструкторів?


48

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

Тоді питання: Чому c ++ не встановлює всі деструктори віртуальними за замовчуванням? або з інших питань:

Коли мені НЕ потрібно використовувати віртуальні деструктори?

У якому випадку я НЕ повинен використовувати віртуальні деструктори?

Яка вартість використання віртуальних деструкторів, якщо я використовую її, навіть якщо вона не потрібна?


6
А що робити, якщо ваш клас не повинен передаватися у спадок? Подивіться на багато стандартних бібліотечних класів, мало хто має віртуальні функції, оскільки вони не призначені для успадковування.
Якийсь програміст чувак

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

1
@underscore_d З типовими реалізаціями, буде створений додатковий код для будь-якого поліморфного класу, якщо всі подібні неявні речі не були девіртуалізовані та оптимізовані. У загальних ABI, це включає щонайменше одного vtable для кожного класу. Макет класу також повинен бути змінений. Після публікації такого класу, як частини якогось загальнодоступного інтерфейсу, ви не можете надійно повернутися, оскільки його зміна знову призведе до порушення сумісності з ABI, оскільки, очевидно, погано (якщо це можливо) очікувати девіатуризації як інтерфейсних контрактів взагалі.
FrankHB

1
@underscore_d Фраза "під час компіляції" є неточною, але я думаю, що це означає, що віртуальний деструктор не може бути тривіальним і не вказаним constexpr, тому додаткового генерації коду важко уникнути (якщо ви зовсім не уникаєте знищення таких об'єктів взагалі), так це більш-менш шкодить виконанню часу виконання.
FrankHB

2
@underscore_d "Покажчик" здається червоним оселедцем. Це, можливо, має бути вказівником на члена (який не є вказівником за визначенням). Зі звичайними ABI вказівник на члена часто не вписується в машинне слово (як типові покажчики), а зміна класу з неполіморфного на поліморфний часто мінятиме розмір вказівника на члена цього класу.
FrankHB

Відповіді:


41

Якщо ви додасте віртуальний деструктор до класу:

  • у більшості (усіх?) поточних реалізацій C ++, кожен екземпляр об'єкта цього класу повинен зберігати покажчик на таблицю віртуальної диспетчеризації для типу виконання, а сама таблиця віртуальної диспетчеризації додана до виконуваного зображення

  • Адреса таблиці віртуальної диспетчеризації не обов'язково діє в усіх процесах, що може перешкоджати безпечному обміну такими об'єктами у спільній пам'яті

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

  • Самі виклики деструктора за певних умов можуть бути вислані фактично і, отже, поза мережею, тоді як невіртуальні деструктори можуть бути вкладеними або оптимізованими, якщо тривіально або не має значення для абонента.

Аргумент "не призначений для успадкування від" не був би практичною причиною того, що не завжди було віртуального деструктора, якщо воно також не було б гіршим практичним способом, як пояснено вище; але, якщо це гірше, це головний критерій, коли потрібно оплатити вартість: за замовчуванням мати віртуальний деструктор, якщо ваш клас призначений для використання в якості базового класу . Це не завжди необхідно, але це гарантує, що класи в спадковій мірі можна використовувати більш вільно, без випадкової невизначеної поведінки, якщо похідний деструктор класів викликається за допомогою вказівника або посилання базового класу.

"у більшості випадків деструктори повинні бути віртуальними"

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


23

У якому випадку я НЕ повинен використовувати віртуальні деструктори?

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

До речі,

У якому випадку слід використовувати віртуальні деструктори?

Для базових класів з поліморфною делецією.


7
+1 для №2, зокрема без поліморфного видалення . Якщо ваш деструктор ніколи не може бути викликаний за допомогою базового вказівника, це робить його віртуальним непотрібним і зайвим, особливо якщо ваш клас раніше не був віртуальним (тому він стає щойно роздутим RTTI). Щоб захистити від будь-якого користувача, який порушує це, як радив Герб Саттер, ви повинні зробити dtor базового класу захищеним і невіртуальним, так що на нього може бути викликаний / після похідного деструктора.
підкреслюй_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
};

Всього 16 Integerкласів для цього класу, а не 4 байти. Якщо ми зберегли мільйон таких в масиві, ми отримаємо 16 мегабайт пам'яті: вдвічі більший розмір типового кеш-пам'яті 83 L3 процесора, і повторення через такий масив повторно може бути в багато разів повільніше, ніж 4-мегабайтний еквівалент без віртуального вказівника в результаті додаткових пропусків кешу та помилок сторінки.

Однак ціна віртуального вказівника на об'єкт не збільшується при збільшенні кількості віртуальних функцій. У вас може бути 100 функцій віртуального члена в класі, а накладні екземпляри все одно будуть одним віртуальним вказівником.

Віртуальний вказівник, як правило, є більш безпосереднім питанням з точки зору накладних витрат. Однак, крім віртуального вказівника на екземпляр - це вартість за клас. Кожен клас з віртуальними функціями генерує vtableпам'ять, яка зберігає адреси до функцій, які він повинен насправді викликати (віртуальна / динамічна відправка) при здійсненні виклику віртуальної функції. Потім vptrзбережений екземпляр вказує на цей клас vtable. Ця накладні витрати, як правило, викликають менше занепокоєння, але це може збільшити ваш бінарний розмір і додати трохи витрат часу виконання, якщо ця накладна оплата буде марно оплачуватися за тисячу класів у складному кодовому базі, наприклад, ця vtableсторона витрат насправді збільшується пропорційно більше і більше більше віртуальних функцій у суміші.

Розробники Java, які працюють у критичних для продуктивності областях, дуже добре розуміють цей вид накладних витрат (хоча часто описуються в контексті боксу), оскільки визначений користувачем тип Java неявно успадковується від центрального objectбазового класу, а всі функції на Java явно віртуально (заміна ) у природі, якщо не зазначено інше. Як результат, Java Integerтакож вимагає 16 байт пам'яті на 64-розрядних платформах в результаті цього vptrметаданих-стилів, асоційованих за екземпляром, і, як правило, в Java неможливо об'єднати щось подібне intдо класу, не платячи час виконання вартість виконання для цього.

Тоді питання: Чому c ++ не встановлює всі деструктори віртуальними за замовчуванням?

C ++ дійсно сприяє продуктивності, що складається з способу «оплати, коли ти йдеш», а також все ще безліч конструкцій з металевим обладнанням, що керуються голими металами, успадкованих від C. Це не хоче зайво включати накладні витрати, необхідні для генерації Vtable та динамічної відправки для кожен задіяний клас / екземпляр. Якщо продуктивність не є однією з ключових причин, коли ви використовуєте таку мову, як C ++, ви можете отримати більше переваг від інших мов програмування там, оскільки багато мови C ++ менш безпечні та складніші, ніж це в ідеалі, а продуктивність часто є ключова причина прихильності такого дизайну.

Коли мені НЕ потрібно використовувати віртуальні деструктори?

Доволі часто. Якщо клас не розрахований на спадкування, він не потребує віртуального деструктора, а лише платить за можливі великі накладні витрати за те, що йому не потрібно. Так само, навіть якщо клас призначений для спадкування, але ви ніколи не видаляєте екземпляри підтипу через базовий покажчик, то він також не потребує віртуального деструктора. У цьому випадку безпечною практикою є визначення захищеного невіртуального деструктора, наприклад:

class BaseClass
{
protected:
    // Disallow deleting/destroying subclass objects through `BaseClass*`.
    ~BaseClass() {}
};

У якому випадку я НЕ повинен використовувати віртуальні деструктори?

Насправді простіше висвітлити, коли слід використовувати віртуальні деструктори. Досить часто набагато більше класів у вашій кодовій базі не розраховані на спадкування.

std::vectorНаприклад, не розроблено для спадкування і зазвичай не має бути успадкованим (дуже хитка конструкція), оскільки це буде схильне до проблеми видалення базового вказівника ( std::vectorнавмисно уникає віртуального деструктора) на додаток до незграбних проблем з нарізанням об'єктів, якщо ваш похідний клас додає будь-який новий стан.

Загалом клас, який успадковується, повинен мати або відкритий віртуальний деструктор, або захищений, невіртуальний. З C++ Coding Standards, глава 50:

50. Зробіть деструктори базового класу публічними та віртуальними, або захищеними та невіртуальними. Видалити або не видалити; це питання: Якщо слід дозволити видалення через покажчик до базової бази, то деструктор Base повинен бути відкритим та віртуальним. В іншому випадку він повинен бути захищеним і невіртуальним.

Одне з речей C ++ має на увазі неявне підкреслення (оскільки проекти, як правило, стають справді крихкими і незграбними, а можливо, навіть небезпечними в іншому випадку), - це думка, що успадкування не є механізмом, призначеним для використання в якості задуму. Це механізм розширення з урахуванням поліморфізму, але той, який вимагає передбачення того, де потрібна розширюваність. Як результат, ваші базові класи повинні бути розроблені як коріння ієрархії спадкування наперед, а не те, що ви успадковуєте пізніше, як заздалегідь, без такого передбачення заздалегідь.

У тих випадках, коли ви просто хочете успадкувати повторне використання існуючого коду, композиція часто наполегливо рекомендується (Композиційний принцип повторного використання).


9

Чому c ++ за замовчуванням не встановлює всі віртуальні деструктори? Вартість додаткового зберігання та виклику таблиці віртуальних методів. C ++ використовується для системного, низької затримки, rt програмування, де це може бути тягарем.


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

9
@MarcoA. З якого часу деструктори мають на увазі динамічне розподілення пам'яті?
chbaker0

@ chbaker0 Я використовував "як". Вони просто не використовуються в моєму досвіді.
Марко А.

6
Також є дурницею, що динамічну пам'ять не можна використовувати в жорстких системах реального часу. Досить тривіально довести, що заздалегідь налаштована купа з фіксованими розмірами розподілу та бітовою картою розподілу буде або виділяти пам'ять, або повертати стан поза пам'яті протягом часу, необхідного для сканування цієї растрової карти.
MSalters

@msalters, що мене змушує задуматися: уявіть собі програму, де вартість кожної операції зберігалася в системі типів. Дозвіл перевірки часу складання гарантій у режимі реального часу.
Якк

5

Це хороший приклад того, коли не використовувати віртуальний деструктор: From Scott Meyers:

Якщо клас не містить віртуальних функцій, це часто є ознакою того, що він не призначений для використання в якості базового класу. Коли клас не призначений для використання в якості базового класу, зробити деструктор віртуальним зазвичай погана ідея. Розглянемо цей приклад, грунтуючись на дискусії в АРМ:

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

Якщо короткий 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.Вут. Хтось ще пам’ятає Старі добрі часи, де нам було дозволено використовувати класи та успадкування для створення послідовних шарів багаторазових членів та поведінки, не піклуючись про віртуальні методи взагалі? Хай, Скотт. Я розумію основну точку, але це "часто" дійсно досягає.
підкреслюй__29

3

Віртуальний деструктор додає вартість виконання. Вартість особливо велика, якщо в класі немає інших віртуальних методів. Віртуальний деструктор також потрібен лише в одному конкретному сценарії, де об’єкт видаляється або іншим чином знищується через покажчик на базовий клас. У цьому випадку деструктор базового класу повинен бути віртуальним, а деструктор будь-якого похідного класу буде неявно віртуальним. Існує кілька сценаріїв, коли поліморфний базовий клас використовується таким чином, що деструктору не потрібно бути віртуальним:

  • Якщо екземпляри похідних класів не виділяються на купі, наприклад, лише безпосередньо в стеку або всередині інших об'єктів. (За винятком випадків, коли ви використовуєте неініціалізовану пам'ять і оператор розміщення new.)
  • Якщо екземпляри похідних класів виділяються на купі, але видалення відбувається лише за допомогою покажчиків на найбільш похідний клас, наприклад, є std::unique_ptr<Derived>, а поліморфізм відбувається лише через невласні покажчики та посилання. Інший приклад - коли об’єкти виділяються за допомогою std::make_shared<Derived>(). Це добре використовувати std::shared_ptr<Base>так само, як початковий покажчик був a std::shared_ptr<Derived>. Це пояснюється тим, що спільні покажчики мають власну динамічну диспетчеризацію деструкторів (делетера), яка не обов'язково покладається на віртуальний деструктор базового класу.

Звичайно, будь-яка умова використовувати предмети лише вищезазначеними способами легко може бути порушена. Тому порада Герба Саттера залишається такою ж актуальною: "Деструктори базового класу повинні бути або публічними, і віртуальними, або захищеними і невіртуальними". Таким чином, якщо хтось спробує видалити покажчик на базовий клас з невіртуальним деструктором, він, швидше за все, отримає помилку порушення доступу під час компіляції.

Потім знову з’являються класи, які не призначені для (загальнодоступних) базових класів. Моя особиста рекомендація - вносити їх finalу С ++ 11 або вище. Якщо він розроблений як квадратний кілочок, швидше за все, він буде працювати не дуже добре, як круглий кілочок. Це пов'язано з моїм уподобанням мати чіткий договір спадкування між базовим класом і похідним класом, для моделі дизайну NVI (невіртуального інтерфейсу), абстрактних, а не конкретних базових класів, і моїм відхиленням захищених змінних членів, серед іншого , але я знаю, що всі ці погляди певною мірою суперечать.


1

Декларація деструктора virtualнеобхідна лише тоді, коли ви плануєте зробити classспадщину. Зазвичай класи стандартної бібліотеки (такі як std::string) не забезпечують віртуального деструктора і, таким чином, не призначені для підкласифікації.


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

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

1

У конструкторі буде накладні витрати для створення vtable (якщо у вас немає інших віртуальних функцій; у такому випадку ви ПРОБАВНО, але не завжди повинні мати і віртуальний деструктор). І якщо у вас немає інших віртуальних функцій, це робить ваш об’єкт на один розмір вказівника більшим, ніж потрібно. Очевидно, що збільшений розмір може мати великий вплив на дрібні предмети.

Існує додаткова пам'ять, яку читають, щоб отримати 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це грубе надмірне спрощення - і передчасна песимізація . Це потрібно лише в тому випадку, якщо комусь дозволено видалити похідний клас через покажчик на базу. У багатьох ситуаціях це не так. Якщо ви знаєте, що це так, то обов'язково покладіть накладні витрати. Що, btw, завжди додається, навіть якщо фактичні виклики можуть статично вирішити компілятор. В іншому випадку, коли ви правильно контролюєте, що люди можуть робити з вашими предметами, це не варто
підкреслюйте
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.