Яка вартість використання віртуальних деструкторів, якщо я використовую її, навіть якщо вона не потрібна?
Вартість введення будь-якої віртуальної функції до класу (успадкованого або частини визначення класу), можливо, дуже крута (або не залежить від об'єкта) початкова вартість віртуального вказівника, що зберігається на один об'єкт, наприклад:
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 ++ має на увазі неявне підкреслення (оскільки проекти, як правило, стають справді крихкими і незграбними, а можливо, навіть небезпечними в іншому випадку), - це думка, що успадкування не є механізмом, призначеним для використання в якості задуму. Це механізм розширення з урахуванням поліморфізму, але той, який вимагає передбачення того, де потрібна розширюваність. Як результат, ваші базові класи повинні бути розроблені як коріння ієрархії спадкування наперед, а не те, що ви успадковуєте пізніше, як заздалегідь, без такого передбачення заздалегідь.
У тих випадках, коли ви просто хочете успадкувати повторне використання існуючого коду, композиція часто наполегливо рекомендується (Композиційний принцип повторного використання).