Подтвердить что ты не робот

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

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

4b9b3361

Ответ 1

Вызов деструктора вручную требуется, если объект был создан с использованием перегруженной формы 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);

Внешнее управление памятью на довольно низком уровне, как указано выше, вызывает деструкторы явно, однако, является признаком плохого дизайна. Вероятно, на самом деле это не просто плохой дизайн, но совершенно неправильный (да, использование явного деструктора, за которым следует вызов конструктора копирования в операторе присваивания, является плохим дизайном и может быть неправильным).

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

Ответ 2

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

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

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

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

Ответ 4

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

Eg.

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

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

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

Ответ 5

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

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

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

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

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

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

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

При создании нового объекта вы получаете кусок памяти из предварительно выделенного пула и выполняете "размещение нового". После выполнения объекта вы можете явно вызвать деструктор, чтобы завершить работу по очистке, если таковая имеется. Но вы фактически не освободите память, как это сделал бы оператор delete. Вместо этого вы возвращаете кусок в пул для повторного использования.

Ответ 6

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

Ответ 7

Бывают случаи, когда они необходимы:

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

Ответ 8

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

Ответ 9

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

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

Ответ 10

Как насчет этого?
Деструктор не вызывается, если из конструктора выбрано исключение, поэтому я должен вызвать его вручную, чтобы уничтожить дескрипторы, созданные в конструкторе до исключения.

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);
  }
};

Ответ 11

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

При написании типа метода "Reset" для восстановления объекта в его исходное состояние вполне разумно вызвать деструктор для удаления старых данных 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.
    }
};

Ответ 12

Память не отличается от другого ресурса: вы должны взглянуть на http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Keynote-Bjarne-Stroustrup-Cpp11-Style, особенно на ту часть, где Бьярне говорит о RAII (около ~ 30мин)

Все необходимые шаблоны (shared_ptr, unique_ptr, weak_ptr) являются частью стандартной библиотеки С++ 11