Всегда ли вызов деструктора является признаком плохого дизайна?
Я думал: они говорят, что если вы вызываете деструктор вручную - вы делаете что-то не так. Но так ли это всегда? Есть ли контрпримеры? Ситуации, когда необходимо вызвать его вручную или когда трудно / невозможно / нецелесообразно избежать этого?
13 ответов
Вызов деструктора вручную требуется, если объект был создан с использованием перегруженной формы 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 при изменении типа представляемого объекта. Также, когда объединение уничтожено, необходимо явно вызвать деструктор текущего объекта, если он требует уничтожения.
Все ответы описывают конкретные случаи, но есть общий ответ:
Вы вызываете 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
покупает в.
Это плохой дизайн, если это происходит случайным образом вокруг кода, это хороший дизайн, если это происходит локально с классами, специально разработанными для этой цели.
Нет, зависит от ситуации, иногда это законный и хороший дизайн.
Чтобы понять, почему и когда вам нужно явно вызывать деструкторы, давайте посмотрим, что происходит с "new" и "delete".
Чтобы создать объект динамически, T* t = new T;
Под капотом: 1. Размер памяти (T) выделен. 2. Конструктор T вызывается для инициализации выделенной памяти. Оператор new делает две вещи: выделение и инициализация.
Уничтожить объект delete t;
под капотом: 1. Т деструктор называется. 2. память, выделенная для этого объекта освобождается. Оператор удаления также делает две вещи: уничтожение и освобождение.
Один пишет конструктор для инициализации и деструктор для уничтожения. Когда вы явно вызываете деструктор, выполняется только уничтожение, но не освобождение.
Следовательно, законным использованием явно вызывающего деструктора может быть: "Я только хочу уничтожить объект, но я не (или не могу) освободить выделение памяти (пока)".
Типичным примером этого является предварительное выделение памяти для пула определенных объектов, которые в противном случае должны распределяться динамически.
При создании нового объекта вы получаете кусок памяти из предварительно выделенного пула и выполняете "размещение нового". После завершения работы с объектом вы можете явно вызвать деструктор, чтобы завершить очистку, если таковая имеется. Но вы на самом деле не будете освобождать память, как сделал бы оператор удаления. Вместо этого вы возвращаете кусок в пул для повторного использования.
Нет, вы не должны вызывать его явно, потому что он будет вызван дважды. Один раз для ручного вызова и другой раз, когда заканчивается область действия объекта.
Например.
{
Class c;
c.~Class();
}
Если вам действительно необходимо выполнить те же операции, у вас должен быть отдельный метод.
Существует определенная ситуация, в которой вы можете вызвать деструктор для динамически размещаемого объекта с размещением new
но это не звучит то, что вам когда-либо понадобится.
Как указано в FAQ, вы должны явно вызывать деструктор при использовании размещения new.
Это единственный раз, когда вы явно вызываете деструктор.
Я согласен, хотя это редко требуется.
В любое время, когда вам нужно отделить распределение от инициализации, вам нужно будет вручную разместить новый и явный вызов деструктора. Сегодня это редко необходимо, поскольку у нас есть стандартные контейнеры, но если вам нужно внедрить какой-то новый вид контейнера, он вам понадобится.
Найден другой пример, где вам придется вызывать деструктор (ы) вручную. Предположим, вы реализовали вариантный класс, который содержит один из нескольких типов данных:
struct Variant {
union {
std::string str;
int num;
bool b;
};
enum Type { Str, Int, Bool } type;
};
Если Variant
Экземпляр держал std::string
и теперь вы присваиваете объединению другой тип, вы должны уничтожить std::string
первый. Компилятор не будет делать это автоматически.
Есть случаи, когда они необходимы:
В коде, над которым я работаю, я использую явный вызов деструктора в распределителях, у меня есть реализация простого распределителя, который использует размещение 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() с использованием платформозависимых механизмов alloc и dealloc. Этот распределитель использовался для обхода doug lea malloc и непосредственного использования, например, LocalAlloc для windows.
Как насчет этого?
Деструктор не вызывается, если из конструктора выдается исключение, поэтому я должен вызвать его вручную, чтобы уничтожить дескрипторы, которые были созданы в конструкторе перед исключением.
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);
}
};
Я нашел 3 случая, когда мне нужно было сделать это:
- распределение / освобождение объектов в памяти, созданной с помощью memory-mapped-io или разделяемой памяти
- при реализации данного интерфейса C с использованием C++ (да, к сожалению, это все еще происходит сегодня (потому что у меня недостаточно влияния, чтобы изменить его))
- при реализации классов-распределителей
Я никогда не сталкивался с ситуацией, когда нужно вызывать деструктор вручную. Кажется, я помню, что даже Страуструп утверждает, что это плохая практика.
Память ничем не отличается от других ресурсов: вы должны взглянуть на http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Keynote-Bjarne-Stroustrup-Cpp11-Style особенно в той части, где Бьярне говорит о RAII (около 30 минут)
Все необходимые шаблоны (shared_ptr, unique_ptr, weak_ptr) являются частью стандартной библиотеки C++11
У меня есть другая ситуация, когда я думаю, что вполне разумно называть деструктором.
При написании метода "Reset" для восстановления объекта до его начального состояния совершенно разумно вызывать Destructor, чтобы удалить старые данные, которые сбрасываются.
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.
}
};