Чи завжди виклик деструктора завжди є ознакою поганого дизайну?


83

Я думав: мовляв, якщо ви викликаєте деструктор вручну - ви робите щось не так. Але чи завжди це так? Чи є зустрічні приклади? Ситуації, коли необхідно зателефонувати йому вручну або де складно / неможливо / непрактично уникнути цього?


Як ви збираєтеся вивільнити об’єкт після виклику dtor, не викликаючи його знову?
ssube

2
@peachykeen: ви зателефонуєте розміщенню, newщоб ініціалізувати новий об'єкт замість старого. Як правило, це не гарна ідея, але це не нечуване.
D.Shawley

14
Подивіться на "правила", що містять слова "завжди" і "ніколи", які не походять безпосередньо із специфікацій із підозрою: у більшості випадків той, хто їх навчає, хоче приховати вам те, що ви повинні знати, але він цього не робить знати, як навчати. Так само, як дорослий відповідає дитині на питання про секс.
Emilio Garavaglia

Я думаю, це добре в разі маніпуляцій з побудовою об’єктів за допомогою техніки розміщення stroustrup.com/bs_faq2.html#placement-delete (але це досить низький рівень і використовується лише тоді, коли ви оптимізуєте своє програмне забезпечення навіть на такому рівні)
bruziuz

Відповіді:


94

Виклик деструктора вручну необхідний, якщо об'єкт був побудований з використанням перевантаженої форми operator new(), крім випадків, коли використовується " std::nothrow" перевантаження:

T* t0 = new(std::nothrow) T();
delete t0; // OK: std::nothrow overload

void* buffer = malloc(sizeof(T));
T* t1 = new(buffer) T();
t1->~T(); // required: delete t1 would be wrong
free(buffer);

Однак зовнішнє управління пам'яттю на досить низькому рівні, як зазначено вище, явно викликаючи деструктори, є ознакою поганого дизайну. Можливо, це насправді не просто поганий дизайн, а відверто неправильний (так, використання явного деструктора з наступним викликом конструктора копії в операторі присвоєння є поганим дизайном і, ймовірно, буде помилковим).

У C ++ 2011 є ще одна причина використовувати явні виклики деструктора: При використанні узагальнених об'єднань необхідно явно знищити поточний об'єкт і створити новий об'єкт, використовуючи розміщення new, при зміні типу представленого об'єкта. Крім того, коли об'єднання руйнується, необхідно явно викликати деструктор поточного об'єкта, якщо він вимагає знищення.


26
Замість того, щоб говорити "за допомогою перевантаженої форми operator new", правильною фразою є "використання placement new".
Remy Lebeau

5
@RemyLebeau: Ну, я хотів пояснити, що я говорю не тільки про operator new(std::size_t, void*)(і про варіацію масиву), а про всю перевантажену версію operator new().
Дітмар Кюль

Як щодо того, коли ви хочете скопіювати об’єкт, щоб виконати в ньому операцію, не змінюючи його під час обчислення? temp = Class(object); temp.operation(); object.~Class(); object = Class(temp); temp.~Class();
Jean-Luc Nacif Coelho

yes, using an explicit destructor followed by a copy constructor call in the assignment operator is a bad design and likely to be wrong. Чому ви так говорите? Я вважаю, що якщо деструктор є тривіальним або близьким до тривіального, він має мінімальні накладні витрати та збільшує використання принципу СУХОСТІ. Якщо використовувати в таких випадках з переміщенням operator=(), це може бути навіть краще, ніж використання обміну. YMMV.
Адріан

1
@Adrian: виклик деструктора та відтворення об'єкта дуже легко змінює тип об'єкта: він відтворить об'єкт із статичним типом призначення, але динамічний тип може бути іншим. Це насправді проблема, коли клас має virtualфункції ( virtualфункції не відтворюватимуться), а в іншому випадку об’єкт лише частково [повторно] будується.
Дітмар Кюль

101

Усі відповіді описують конкретні випадки, але є загальна відповідь:

Ви викликаєте dtor явно кожного разу, коли вам потрібно просто знищити об'єкт (у сенсі C ++), не звільняючи пам'яті, в якій знаходиться об'єкт.

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

Ось необроблений приклад:

{
  char buffer[sizeof(MyClass)];

  {
     MyClass* p = new(buffer)MyClass;
     p->dosomething();
     p->~MyClass();
  }
  {
     MyClass* p = new(buffer)MyClass;
     p->dosomething();
     p->~MyClass();
  }

}

Іншим помітним прикладом є значення за замовчуванням std::allocatorпри використанні std::vector: елементи будуються в vectorпід час push_back, але пам'ять виділяється шматками, тому вона вже існує для створення елементів. А отже, vector::eraseповинен знищити елементи, але не обов'язково це звільняє пам'ять (особливо якщо новий push_back повинен відбутися незабаром ...).

Це "поганий дизайн" у суворому ООП-розумінні (ви повинні керувати об'єктами, а не пам'яттю: факт, що об'єкти вимагають пам'яті, - це "інцидент"), це "хороший дизайн" у "програмуванні низького рівня" або у випадках, коли пам'ять не взято з "безкоштовного магазину" за замовчуванням operator new купується в.

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


8
Просто цікаво, чому це не прийнята відповідь.
Френсіс Куглер

11

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

Напр.

{
  Class c;
  c.~Class();
}

Якщо вам дійсно потрібно виконати ті самі операції, ви повинні мати окремий метод.

Існує конкретна ситуація, коли ви можете викликати деструктор на динамічно виділеному об’єкті з розташуванням, newале це не звучить так, як вам коли-небудь знадобиться.


11

Ні, це залежить від ситуації, іноді це законний і хороший дизайн.

Щоб зрозуміти, чому і коли потрібно явно викликати деструктори, давайте подивимося, що відбувається з "new" і "delete".

Для динамічного створення об’єкта T* t = new T;під капотом: 1. виділено пам’ять sizeof (T). 2. Конструктор Т викликається для ініціалізації виділеної пам'яті. Оператор new робить дві речі: розподіл та ініціалізацію.

Для знищення об’єкта delete t;під капотом: 1. Викликається деструктор Т. 2. звільняється пам’ять, виділена для цього об’єкта. оператор delete також робить дві речі: знищення та вивільнення.

Один пише конструктор для ініціалізації, а деструктор - для знищення. Коли ви явно викликаєте деструктор, виконується лише руйнування, але не вивільнення .

Отже, законним використанням явного виклику деструктора може бути: "Я хочу лише знищити об'єкт, але я не (або не можу) звільнити виділення пам'яті (поки)".

Поширеним прикладом цього є попереднє виділення пам'яті для пулу певних об'єктів, які в іншому випадку повинні бути розподілені динамічно.

Створюючи новий об'єкт, ви отримуєте шматок пам'яті з попередньо виділеного пулу та виконуєте "розміщення нового". Закінчивши з об’єктом, ви можете явно зателефонувати деструктору, щоб закінчити роботу з очищення, якщо така є. Але ви не зможете звільнити пам'ять, як це зробив би оператор видалення. Натомість ви повертаєте шматок до пулу для повторного використання.



6

Кожного разу, коли вам потрібно відокремити розподіл від ініціалізації, вам знадобиться розміщення нового та явного виклику деструктора вручну. Сьогодні це рідко потрібно, оскільки у нас є стандартні контейнери, але якщо вам доведеться застосувати якийсь новий тип контейнера, він вам знадобиться.


3

Бувають випадки, коли вони необхідні:

У коді, над яким я працюю, я використовую явний виклик деструктора в розподільниках, у мене є реалізація простого розподільника, який використовує розміщення new для повернення блоків пам'яті в контейнери stl. У знищення я маю:

  void destroy (pointer p) {
    // destroy objects by calling their destructor
    p->~T();
  }

під час побудови:

  void construct (pointer p, const T& value) {
    // initialize memory with placement new
    #undef new
    ::new((PVOID)p) T(value);
  }

також розподіл здійснюється у системі allocate () та вивільнення пам'яті в deallocate (), використовуючи механізми виділення та вивільнення, специфічні для платформи. Цей розподільник використовувався для обходу doug lea malloc та використання безпосередньо, наприклад, LocalAlloc на вікнах.


1

Я знайшов 3 випадки, коли мені потрібно було це зробити:

  • виділення / вивільнення об'єктів у пам'яті, створеної за допомогою map-map-io або спільної пам'яті
  • при реалізації заданого інтерфейсу C за допомогою C ++ (так, це трапляється і сьогодні, на жаль (тому що у мене недостатньо впливу, щоб змінити його))
  • при реалізації класів розподільника

1

Я ніколи не стикався з ситуацією, коли потрібно викликати деструктор вручну. Здається, я пам’ятаю, що навіть Строуструп стверджує, що це погана практика.


1
Ви праві. Але я використав нове розміщення. Я зміг додати функцію очищення в метод, відмінний від деструктора. Деструктор є, тому його можна "автоматично" викликати при видаленні, коли ви вручну хочете знищити, але не звільнити місце, ви можете просто написати "onDestruct", чи не так? Мені було б цікаво почути, чи є приклади, коли об’єкт повинен був би знищити в деструкторі, тому що іноді вам потрібно буде видалити, а інший раз ви хочете лише знищити, а не
вивести з ладу

І навіть у цьому випадку ви можете зателефонувати onDestruct () зсередини деструктора - тому я все ще не бачу випадку ручного виклику деструктора.
Lieuwe

4
@JimBalter: творець C+
Марк К Кован

@MarkKCowan: що таке C +? Це має бути C ++
Destructor

1

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

class MyClass {
  HANDLE h1,h2;
  public:
  MyClass() {
    // handles have to be created first
    h1=SomeAPIToCreateA();
    h2=SomeAPIToCreateB();        
    try {
      ...
      if(error) {
        throw MyException();
      }
    }
    catch(...) {
      this->~MyClass();
      throw;
    }
  }
  ~MyClass() {
    SomeAPIToDestroyA(h1);
    SomeAPIToDestroyB(h2);
  }
};

1
Це видається сумнівним: коли ваш конструктор працює, ви не знаєте (або можете не знати), які частини об’єкта були побудовані, а які ні. Отже, ви не знаєте, для яких під-об’єктів викликати деструктори, наприклад. Або який із ресурсів, виділених конструктором, виділити.
Вайолет Жираф

@VioletGiraffe, якщо під-об'єкти побудовані на стеку, тобто не з "новим", вони будуть знищені автоматично. В іншому випадку ви можете перевірити, чи мають вони значення NULL, перш ніж знищити їх у деструкторі. Те саме з ресурсами
CITBL

Те, як ви написали ctorтут, неправильне, саме з тієї причини, яку ви самі вказали: якщо розподіл ресурсів не вдається, виникає проблема з очищенням. "Ктор" не повинен телефонувати this->~dtor(). dtorслід викликати для побудованих об'єктів, і в цьому випадку об'єкт ще не побудований. Що б не сталося, чистка ctorповинна займатися. Всередині ctorкоду ви повинні використовувати утиліти, подібні std::unique_ptrдо автоматичного очищення для вас, якщо щось викидає. Зміна HANDLE h1, h2полів у класі для підтримки автоматичного очищення також може бути приємною ідеєю.
quetzalcoatl

Це означає, що ctor повинен виглядати так: MyClass(){ cleanupGuard1<HANDLE> tmp_h1(&SomeAPIToDestroyA) = SomeAPIToCreateA(); cleanupGuard2<HANDLE> tmp_h2(&SomeAPIToDestroyB) = SomeAPIToCreateB(); if(error) { throw MyException(); } this->h1 = tmp_h1.release(); this->h2 = tmp_h2.release(); }і все . Жодне ризиковане ручне очищення, відсутність зберігання ручок у частково побудованому об’єкті, поки все не в безпеці, не є бонусом. Якщо ви змінили HANDLE h1,h2клас на cleanupGuard<HANDLE> h1;etc, то вам може навіть не знадобитися dtorвзагалі.
quetzalcoatl

Реалізація cleanupGuard1та cleanupGuard2залежить від того, що робить відповідна xxxToCreateвіддача та які параметри xxxxToDestroyбере відповідний . Якщо вони прості, можливо, вам навіть не потрібно буде нічого писати, оскільки часто виявляється, що std::unique_ptr<x,deleter()>(або подібний) може зробити трюк за вас в обох випадках.
quetzalcoatl

-2

Знайшов інший приклад, коли вам доведеться викликати деструктор (и) вручну. Припустимо, ви реалізували варіантний клас, який містить один із декількох типів даних:

struct Variant {
    union {
        std::string str;
        int num;
        bool b;
    };
    enum Type { Str, Int, Bool } type;
};

Якщо Variantекземпляр містив a std::string, і тепер ви присвоюєте об’єднанню інший тип, ви повинні знищити std::stringперший. Компілятор не буде робити це автоматично .


-4

У мене є інша ситуація, коли я вважаю, що цілком розумно називати деструктор.

При написанні методу типу "Скинути" для відновлення об'єкта до початкового стану цілком розумно викликати Деструктор для видалення старих даних, які скидаються.

class Widget
{
private: 
    char* pDataText { NULL  }; 
    int   idNumber  { 0     };

public:
    void Setup() { pDataText = new char[100]; }
    ~Widget()    { delete pDataText;          }

    void Reset()
    {
        Widget blankWidget;
        this->~Widget();     // Manually delete the current object using the dtor
        *this = blankObject; // Copy a blank object to the this-object.
    }
};

1
Чи не виглядало б чистіше, якщо б ви оголосили спеціальний cleanup()метод, який буде викликаний у цьому випадку та в деструкторі?
Violet Giraffe

"Спеціальний" метод, який викликається лише у двох випадках? Звичайно ... це звучить абсолютно правильно (/ сарказм). Методи повинні бути узагальненими та мати можливість викликати їх у будь-якому місці. Коли ви хочете видалити об’єкт, немає нічого поганого у тому, щоб викликати його деструктор.
abelenky

4
Ви не повинні чітко викликати деструктор у цій ситуації. У будь-якому випадку вам доведеться реалізувати оператор присвоєння.
Ремі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.