Вызвать деструктор, а затем конструктор (сброс объекта)

Я хочу сбросить объект. Могу ли я сделать это следующим образом?

anObject->~AnObject();
anObject = new(anObject) AnObject();
// edit: this is not allowed: anObject->AnObject();

Этот код, очевидно, является подмножеством типичного жизненного цикла объекта, выделенного в месте размещения new:

AnObject* anObject = malloc(sizeof(AnObject));
anObject = new (anObject) AnObject(); // My step 2.
// ...
anObject->~AnObject(); // My step 1.
free(anObject)
// EDIT: The fact I used malloc instead of new doesn't carry any meaning

Единственное, что изменилось, это порядок вызовов конструктора и деструктора.

Итак, почему в следующих часто задаваемых вопросах появляются все угрозы?

[11.9] Но могу ли я явно вызвать деструктор, если я разместил свой объект с помощью new?

FAQ: Нельзя, если только объект не был выделен с размещением new. Объекты, созданные new, должны быть удалены, что делает две вещи (запомните их): вызывает деструктор, а затем освобождает память.

FQA: Перевод: удаление — это способ явного вызова деструктора, но он также освобождает память. Вы также можете вызвать деструктор без освобождения памяти. В большинстве случаев это некрасиво и бесполезно, но вы можете это сделать.

Вызов деструктора/конструктора, очевидно, является обычным кодом C++. Гарантии, используемые в коде, являются прямым результатом размещения новых гарантий. Это ядро стандарта, это надежная вещь. Как его можно назвать "грязным" и представить чем-то ненадежным?

Как вы думаете, возможно ли, что реализация new на месте и без места отличается? Я думаю о какой-то больной возможности, что обычный новый может, например, указать размер блока памяти, выделенного перед блоком, чего, очевидно, новый на месте не сделает (потому что он не выделяет никакой памяти). Это может привести к возникновению пробела для некоторых проблем... Возможна ли такая реализация new()?


person Community    schedule 14.07.2009    source источник
comment
Люди говорят вам, что вы не можете вызвать конструктор. Честно говоря, я никогда-никогда не видел конструктора, вызываемого как в anObject-›AnObject();, но я не знаю стандарта наизусть и не искал его, чтобы подтвердить, что он запрещен. Однако, если вы так уверены, что это ядро стандарта и незыблемая, почему вы спрашиваете? Если вы уверены, это означает, что вы протестировали свой код и он работает, поэтому, если вам нравится это решение, просто используйте его.   -  person Daniel Daranas    schedule 14.07.2009
comment
Я хочу убедиться, что я не пропустил ничего подобного последнему редактированию в вопросе.   -  person    schedule 14.07.2009


Ответы (10)


Не ведитесь на тролля FQA. Он, как обычно, ошибается в фактах.

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

«Обычное» новое/удаление немного упрощает это, связывая выделение памяти и создание объекта вместе, а выделение стека упрощает его еще больше, делая и то, и другое за вас.

Однако совершенно законно следующее:

int foo() {
    CBar bar;
    (&bar)->~CBar();
    new (&bar) CBar(42);
 }

Оба объекта уничтожаются, а память стека также автоматически перерабатывается. однако, в отличие от утверждений FQA, первому вызову деструктора не предшествует размещение new.

person MSalters    schedule 14.07.2009
comment
Как вы думаете, возможно ли, что ин-плейсмент и не-ин-плейсмент new могут различаться? Я думаю о какой-то больной возможности, что обычный новый может, например, указать размер блока памяти, выделенного перед блоком, а новый на месте, очевидно, нет (потому что он не выделяет никакой памяти). Это может привести к пробелу для некоторых проблем... - person ; 14.07.2009
comment
Неважно, чем они отличаются. Реализация должна убедиться, что мой код выше работает, потому что он соответствует требованиям. Пикси-пыль разрешена. - person MSalters; 14.07.2009
comment
Спасибо за ответ. Это рационально. - person ; 14.07.2009
comment
MSalters, возможно, вам будет интересно: общий миксин/функция сброса: stackoverflow.com/questions/1133738/ - person ; 16.07.2009
comment
Почему (&bar)->~CBar(); вместо bar.~CBar();? - person Zero; 20.11.2012
comment
@Zero: оба являются законными и делают то же самое, но изначально в вопросе использовалось ->~, поэтому я не стал это менять. - person MSalters; 20.11.2012
comment
Это на самом деле не безобразно в случае союза, если члены союза требуют построения или уничтожения. - person galinette; 03.04.2014
comment
@galinette: Сейчас это правда, но вопрос возник до C++11. - person MSalters; 03.04.2014
comment
я действительно не понимаю последнее предложение. утверждает ли fqa, что первому вызову деструктора предшествует размещение new? - person 463035818_is_not_a_number; 07.05.2018
comment
@ user463035818: Я считаю автора FQA троллем. Он предполагает, что именно так это и работает, но он достаточно хорошо разбирается в C++, чтобы не говорить об этом прямо. - person MSalters; 08.05.2018

Почему бы не реализовать метод Clear(), который делает все, что делает код в теле деструктора? Затем деструктор просто вызывает Clear(), а вы вызываете Clear() непосредственно для объекта, чтобы "сбросить его".

Другой вариант, если ваш класс правильно поддерживает назначение:

MyClass a;
...
a = MyClass();

Я использую этот шаблон для сброса экземпляров std::stack, поскольку адаптер стека не предоставляет четкую функцию.

person Community    schedule 14.07.2009
comment
Потому что это потребует поддержания корректности метода Clear(). - person ; 14.07.2009
comment
Вы должны поддерживать правильность деструктора — я предлагаю переместить код деструктора в метод Clear(), чтобы деструктор содержал только один вызов Clear(). Я пишу почти весь свой код таким образом, кстати. - person ; 14.07.2009
comment
Нельзя ли реализовать деструктивный метод Clear()? Он должен был бы освободить все элементы, выделенные в куче, при вызове из деструктора. Но при вызове в качестве метода, подобного сбросу, ему нужно будет только очистить такие элементы и оставить их пригодными для использования. Для этого потребуется реализовать метод Init() и вызвать его из конструктора. Метод сброса вызовет Clear(), а затем Init(). Методы инициализации выглядят как код, отличный от RAII. А что, если нужно несколько конструкторов? Это может усложнить ситуацию. - person ; 14.07.2009
comment
Это зависит от семантики класса. Если перестроение класса после очистки обходится дорого, воспользуйтесь моим вторым предложением. Я бы сказал, что любой из них, вероятно, будет менее сложным, чем возиться с явными вызовами деструктора и использованием размещения new. - person ; 14.07.2009

С технической точки зрения явный вызов конструкторов или деструкторов является плохой практикой.

Ключевое слово удаления вызывает их, когда вы его используете. То же самое касается new с конструкторами.

Извините, но этот код заставляет меня рвать на себе волосы. Вы должны делать это так:

Выделить новый экземпляр объекта

AnObject* anObject = new AnObject();

Удалить экземпляр объекта

delete anObject;

НИКОГДА не делайте этого:

anObject->~AnObject(); // My step 1.
free(anObject)

Если вы должны «сбросить» объект, либо создайте метод, который очищает все переменные экземпляра внутри, либо я бы порекомендовал вам удалить объект и выделить себе новый.

"Это ядро ​​языка?"

Это ничего не значит. В Perl есть около шести способов написания цикла for. Тот факт, что вы МОЖЕТЕ делать что-то на языке, потому что он поддерживается, означает, что вы должны его использовать. Черт возьми, я мог бы написать весь свой код, используя операторы for switch, потому что «ядро» языка поддерживает их. Не делает это хорошей идеей.

Почему вы используете malloc, когда вам это явно не нужно. Malloc - это метод C.

Создать и удалить — ваши друзья в C++

«Сброс» объекта

myObject.Reset();

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

person Brock Woolf    schedule 14.07.2009
comment
Я хочу сбросить свой объект и полагаюсь на ядро ​​стандарта С++. Что в этом плохого? - person ; 14.07.2009
comment
Метод Reset() требует обслуживания. - person ; 14.07.2009
comment
Если бы вы написали весь свой код так, как предлагаете, я бы сказал, что вся ваша кодовая база нуждается в обслуживании. - person Brock Woolf; 15.07.2009
comment
Технически, вы должны изменить вторую строку НИКОГДА не делайте эту часть на delete anObject, потому что free() не вызывает деструкторы. - person Dovahkiin; 31.05.2017
comment
Вы можете реализовать Reset с вызовом деструктора и новым размещением. - person Isaac Pascual; 22.02.2018

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

new (anObject) AnObject();

Этот код гарантированно будет хорошо определен, если ячейка памяти все еще доступна — как и должно быть в вашем случае.

(Я удалил часть о том, является ли этот код дискуссионным или нет — он четко определен. Точка.)

Кстати, Брок прав: то, как реализация delete не исправлена ​​— это не то же самое, что и вызов деструктора, за которым следует free. Всегда соединяйте вызовы new и delete, никогда не смешивайте один с другим: это не определено.

person Konrad Rudolph    schedule 14.07.2009
comment
@Mehrdad: что &? :о) Серьезно: спасибо; Я забыл, что anObject уже был указателем. - person Konrad Rudolph; 14.07.2009
comment
Но если мне это нужно (поскольку указатель на сбрасываемый объект хранится в очень длинной очереди, которую я не хочу анализировать), то проблемы, очевидно, нет, верно? - person ; 14.07.2009
comment
Просто чтобы добавить к этому ответу: вся причина, по которой новое размещение существует, заключается в том, что вы не можете напрямую вызывать конструктор. - person dave4420; 14.07.2009
comment
Итак, что произойдет, если вызов размещения нового ctor вызовет исключение? Память освобождаться не будет, но и объект не создается, поэтому мы не можем вызвать удаление самостоятельно. - person jalf; 14.07.2009
comment
@jalf: если конструкторы выдают исключение, им нужно очистить то, что они уже создали - всегда, а не только с новым размещением. После этого нам не нужно, да и не следует вызывать delete на объекте. - person Konrad Rudolph; 14.07.2009

Обратите внимание, что используются не malloc и free, а operator new и operator delete. Кроме того, в отличие от вашего кода, используя new, вы гарантируете безопасность исключений. Почти эквивалентный код будет следующим.

AnObject* anObject = ::operator new(sizeof(AnObject));
try
{
    anObject = new (anObject) AnObject();
}
catch (...)
{
    ::operator delete(anObject);
    throw;
}

anObject->~AnObject();
::operator delete(anObject)

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

person avakar    schedule 14.07.2009

Да, то, что вы делаете, действительно в большинстве случаев. [basic.life]p8 говорит:

Если после того, как время жизни объекта закончилось и до того, как хранилище, которое занимал объект, было повторно использовано или освобождено, новый объект создается в месте хранения, которое занимал исходный объект, указатель, указывающий на исходный объект, ссылка, которая относится к исходному объекту, или имя исходного объекта будет автоматически ссылаться на новый объект и, как только начнется время жизни нового объекта, может использоваться для управления новым объектом, если:

  • хранилище для нового объекта точно перекрывает место хранения, которое занимал исходный объект, и

  • новый объект имеет тот же тип, что и исходный объект (игнорируя cv-квалификаторы верхнего уровня), и

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

  • ни исходный объект, ни новый объект не являются потенциально перекрывающимися подобъектами ([intro.object]).

Так что законно, если у вас нет const или ссылочного члена.

Если у вас нет этой гарантии, вам нужно использовать std::launder или использовать указатель, возвращаемый размещением new (как вы все равно делаете), если вы хотите использовать новый объект:

// no const/ref members
anObject->~AnObject(); // destroy object
new (anObject) AnObject(); // create new object in same storage, ok

anObject->f(); // ok

// const/ref members
anObject->~AnObject();
auto newObject = new (anObject) AnObject();

anObject->f(); // UB
newObject->f(); // ok
std::launder(anObject)->f(); // ok
person Rakete1111    schedule 07.05.2018

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

E.g.

anObject.swap( AnObject() ); // swap with "clean" object
person Logan Capaldo    schedule 14.07.2009
comment
Это единственная реальная альтернатива IMO - HandleBody + swap() с временным. - person ; 14.07.2009

Почему бы не сбросить с помощью оператора = ()? Это не подлежит обсуждению и гораздо более читабельно.

A a;
//do something that changes the state of a
a = A(); // reset the thing
person David Feurle    schedule 08.05.2018

Если ваш объект имеет разумную семантику присваивания (и правильный оператор =), тогда *anObject = AnObject() имеет больше смысла и его легче понять.

person EFraim    schedule 14.07.2009

Гораздо лучше просто добавить что-то вроде метода Reset() к вашему объекту, а не играть с размещением new.

Вы используете новую функцию размещения, которая предназначена для того, чтобы позволить вам контролировать, где размещается объект. Обычно это проблема только в том случае, если ваше оборудование имеет «специальную» память, например флэш-чип. ЕСЛИ вы хотите поместить какие-либо объекты во флэш-чип, вы можете использовать эту технику. Причина, по которой он позволяет вам явно вызывать деструктор, заключается в том, что ВЫ теперь контролируете память, поэтому компилятор C++ не знает, как выполнить часть освобождения при удалении.

Это также не сэкономит вам много кода, с помощью метода сброса вам придется установить элементы в их начальные значения. malloc() этого не делает, поэтому вам все равно придется писать этот код в конструкторе. Просто создайте функцию, которая устанавливает вашим членам начальные значения, вызовите ее Reset(), вызовите ее из конструктора, а также из любого другого места, которое вам нужно.

person manovi    schedule 14.07.2009
comment
Настоящей альтернативой является идиома HandleBody и метод swap(). Метод сброса я считаю вредным -- он требует тщательного ухода. - person ; 14.07.2009