Всегда ли вызов деструктора вручную является признаком плохого дизайна?

84

Я подумал: мол, если вы вызываете деструктор вручную - вы что-то делаете не так. Но всегда ли так? Есть контрпримеры? Ситуации, когда необходимо вызвать его вручную или когда этого трудно / невозможно / нецелесообразно избежать?

Фиолетовый жираф
источник
Как вы собираетесь освободить объект после вызова dtor, не вызывая его снова?
ssube 06
2
@peachykeen: вы вызываете размещение newдля инициализации нового объекта вместо старого. Как правило, это не очень хорошая идея, но это не редкость.
D.Shawley 06
14
Посмотрите на «правила», содержащие слова «всегда» и «никогда», которые не вытекают непосредственно из спецификаций с подозрением: в большинстве случаев, кто их обучает, хочет скрыть от вас то, что вы должны знать, но он не умеют учить. Так же, как взрослый отвечает ребенку на вопрос о сексе.
Эмилио Гаравалья
Я думаю, что это нормально в случае манипуляций с построением объектов с помощью техники размещения stroustrup.com/bs_faq2.html#placement-delete (но это довольно низкоуровневая вещь и используется только тогда, когда вы оптимизируете свое программное обеспечение даже на таком уровне)
bruziuz

Ответы:

95

Вызов деструктора вручную требуется, если объект был создан с использованием перегруженной формы 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».
Реми Лебо
5
@RemyLebeau: Ну, я хотел уточнить, что я говорю не только о operator new(std::size_t, void*)(и вариации массива), но и обо всех перегруженных версиях operator new().
Дитмар Кюль
А как насчет того, чтобы скопировать объект, чтобы выполнить в нем операцию, не изменяя его во время вычисления? temp = Class(object); temp.operation(); object.~Class(); object = Class(temp); temp.~Class();
Жан-Люк Насиф Коэльо
yes, using an explicit destructor followed by a copy constructor call in the assignment operator is a bad design and likely to be wrong. Почему ты говоришь это? Я бы подумал, что если деструктор тривиален или близок к тривиальному, он имеет минимальные накладные расходы и увеличивает использование принципа DRY. Если использовать в таких случаях с ходом operator=(), это может быть даже лучше, чем использование свопа. YMMV.
Адриан
1
@Adrian: вызов деструктора и воссоздание объекта очень легко изменяет тип объекта: он воссоздает объект со статическим типом назначения, но динамический тип может быть другим. На самом деле это проблема, когда у класса есть virtualфункции ( virtualфункции не будут воссозданы), а в противном случае объект просто частично [воссоздан].
Дитмар Кюль
104

Все ответы описывают конкретные случаи, но есть общий ответ:

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

Обычно это происходит во всех ситуациях, когда выделение / освобождение памяти управляется независимо от создания / уничтожения объекта. В этих случаях построение происходит путем размещения new на существующем фрагменте памяти, а разрушение происходит посредством явного вызова 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
Просто любопытно, почему это не принятый ответ.
Фрэнсис Куглер
12

Нет, вы не должны вызывать его явно, потому что он будет вызываться дважды. Один раз для ручного вызова и другой раз, когда область, в которой объявлен объект, заканчивается.

Например.

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

Если вам действительно нужно выполнить одни и те же операции, вам нужен отдельный метод.

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

Джек
источник
11

Нет, зависит от ситуации, иногда это законный и хороший дизайн.

Чтобы понять, почему и когда вам нужно явно вызывать деструкторы, давайте посмотрим, что происходит с «new» и «delete».

Чтобы создать объект динамически, T* t = new T;под капотом: 1. Выделяется память sizeof (T). 2. Конструктор T вызывается для инициализации выделенной памяти. Оператор new выполняет две функции: выделение и инициализацию.

Чтобы уничтожить объект delete t;под капотом: 1. Вызывается деструктор T. 2. освобождается память, выделенная для этого объекта. оператор delete также выполняет две функции: уничтожение и освобождение.

Один пишет конструктор для инициализации и деструктор для разрушения. Когда вы явно вызываете деструктор, выполняется только уничтожение, но не освобождение .

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

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

При создании нового объекта вы получаете кусок памяти из предварительно выделенного пула и выполняете «новое размещение». После завершения работы с объектом вы можете явно вызвать деструктор для завершения работы по очистке, если таковая имеется. Но на самом деле вы не будете освобождать память, как это сделал бы оператор 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 () с использованием механизмов выделения и освобождения памяти, зависящих от платформы. Этот распределитель использовался для обхода malloc и прямого использования, например, LocalAlloc в окнах.

Марцинь
источник
1

Я нашел 3 случая, когда мне нужно было это сделать:

  • выделение / освобождение объектов в памяти, созданной с помощью memory-mapped-io или разделяемой памяти
  • при реализации заданного интерфейса C с использованием C ++ (да, к сожалению, это все еще происходит сегодня (потому что у меня недостаточно влияния, чтобы его изменить))
  • при реализации классов распределителя

источник
1

Я никогда не сталкивался с ситуацией, когда нужно вызывать деструктор вручную. Я, кажется, помню, что даже Страуструп утверждал, что это плохая практика.

Lieuwe
источник
1
Ты прав. Но я использовал новое размещение. Мне удалось добавить функцию очистки в метод, отличный от деструктора. Деструктор существует для того, чтобы его можно было «автоматически» вызвать при удалении, когда вы вручную хотите уничтожить, но не освободить, вы можете просто написать «onDestruct», не так ли? Мне было бы интересно услышать, есть ли примеры, в которых объект должен был бы уничтожать в деструкторе, потому что иногда вам нужно было бы удалить, а в других случаях вы хотели бы только уничтожить, а не освобождать ..
Lieuwe,
И даже в этом случае вы можете вызвать onDestruct () из деструктора, поэтому я все еще не вижу случая для ручного вызова деструктора.
Льюве
4
@JimBalter: создатель C+
Марк К. Коуэн
@MarkKCowan: что такое C +? Это должен быть C ++
Деструктор
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);
  }
};
CITBL
источник
1
Это кажется сомнительным: когда ваш конструктор работает, вы не знаете (или можете не знать), какие части объекта были созданы, а какие нет. Таким образом, вы не знаете, например, для каких подобъектов вызывать деструкторы. Или какой из ресурсов, выделенных конструктором, освободить.
Violet Giraffe
@VioletGiraffe, если подобъекты построены в стеке, то есть не с «новым», они будут уничтожены автоматически. В противном случае вы можете проверить, равны ли они NULL, прежде чем уничтожить их в деструкторе. То же самое с ресурсами
CITBL 01
То, как вы написали ctorздесь, неверно, именно по той причине, которую вы указали сами: если распределение ресурсов не удается, возникает проблема с очисткой. "Ctor" не должен звонить this->~dtor(). dtorдолжен вызываться для построенных объектов, и в этом случае объект еще не построен. Что бы ни случилось, ctorочистка должна выполняться. Внутри ctorкода вы должны использовать такие утилиты, как std::unique_ptrавтоматическая очистка для вас в случаях, когда что-то бросает. Также HANDLE h1, h2неплохо было бы изменить поля в классе для поддержки автоматической очистки.
Кетцалькоатль
Это означает, что 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.
Кетцалькоатль
Реализация cleanupGuard1и cleanupGuard2зависит от того, что дает релевантный xxxToCreateдоход и какие параметры xxxxToDestroyпринимают соответствующие значения . Если они простые, вам может даже не понадобиться ничего писать, так как часто оказывается, что std::unique_ptr<x,deleter()>(или аналогичный) может помочь вам в обоих случаях.
Кетцалькоатль
-2

Нашел еще один пример, в котором вам придется вызывать деструкторы вручную. Предположим, вы реализовали вариантный класс, содержащий один из нескольких типов данных:

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

Если Variantэкземпляр содержал a std::string, а теперь вы назначаете объединению другой тип, вы должны уничтожить std::stringпервый. Компилятор не сделает этого автоматически .

Фиолетовый жираф
источник
-4

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

При написании метода типа «Reset» для восстановления объекта в его начальное состояние вполне разумно вызвать деструктор для удаления старых данных, которые сбрасываются.

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
В этой ситуации нельзя явно вызывать деструктор. В любом случае вам придется реализовать оператор присваивания.
Реми