Разрешено ли явно вызвать деструктор, за которым следует размещение new на переменной с фиксированным временем жизни?
Я знаю, что вызов деструктора явно может привести к поведению undefined из-за двойного вызова деструктора, как здесь:
#include <vector>
int main() {
std::vector<int> foo(10);
foo.~vector<int>();
return 0; // Oops, destructor will be called again on return, double-free.
}
Но что, если мы будем называть новое слово "воскрешать" объект?
#include <vector>
int main() {
std::vector<int> foo(10);
foo.~vector<int>();
new (&foo) std::vector<int>(5);
return 0;
}
Более формально:
- Что произойдет в С++ (меня интересуют как С++ 03, так и С++ 11, если есть разница), если я явно вызываю деструктор на каком-то объекте, который не был создан с использованием размещения new в первом (например, это либо локальная/глобальная переменная, либо выделена с помощью
new
), а затем, прежде чем этот объект будет разрушен, вызовите новое место на нем, чтобы "восстановить" его?
- Если все в порядке, то гарантируется, что все неконстантные ссылки на этот объект также будут в порядке, если я не использую их, пока объект "мертв"?
- Если да, можно ли использовать одну из неконстантных ссылок для размещения new, чтобы воскресить объект?
- Что относительно ссылок на const?
Пример usecase (хотя этот вопрос больше связан с любопытством): Я хочу "повторно назначить" объект, который не имеет operator=
.
Я видел этот вопрос, в котором говорится, что объект "переопределения", который имеет нестатические члены const
, является незаконным. Итак, позвольте ограничить область действия этого вопроса объектами, которые не имеют каких-либо членов const
.
Ответы
Ответ 1
Во-первых, [basic.life]/8
четко заявляет, что любые указатели или ссылки на исходный foo
должны ссылаться на новый объект, который вы создаете в foo
в вашем случае. Кроме того, имя foo
будет ссылаться на новый объект, построенный там (также [basic.life]/8
).
Во-вторых, вы должны убедиться, что перед исходным типом объекта есть объект исходного типа, который используется для foo
; поэтому, если что-то бросает, вы должны поймать его и завершить свою программу ([basic.life]/9
).
В целом, эта идея часто соблазнительна, но почти всегда является ужасной идеей.
-
(8) Если после того, как время жизни объекта закончилось и до хранения, которое объект занял, повторно используется или выпущен, создается новый объект в месте хранения, в котором был загружен исходный объект, указатель, который указала на исходный объект, ссылку, относящуюся к исходному объекту, или имя исходного объекта будет автоматически ссылаться на новый объект и, как только время жизни нового объекта будет запущено, можно использовать для управления новым объектом, если:
- (8.1) хранилище для нового объекта точно накладывает место хранения, которое было занято исходным объектом, и
- (8.2) новый объект имеет тот же тип, что и исходный объект (игнорируя cv-квалификаторы верхнего уровня) и
- (8.3) тип исходного объекта не является константным, а, если тип класса, не содержит каких-либо нестатических член данных, тип которого является константным или ссылочным, и
- (8.4) исходный объект был наиболее производным объектом (1.8) типа T и новый объект является наиболее производным объект типа T (т.е. они не являются подобъектами базового класса).
-
(9) Если программа завершает время жизни объекта типа T со статическим (3.7.1), потоковым (3.7.2) или автоматическим (3.7.3) временем хранения и если T имеет нетривиальный деструктор, программа должна гарантировать, что объект оригинальный тип занимает то же место хранения, когда имеет место неявный вызов деструктора; в противном случае поведение программы undefined. Это верно, даже если этот блок завершен с исключением.
Есть причины вручную запускать деструкторы и делать новое размещение. Что-то простое, как operator=
, не является одним из них, если вы не пишете свой собственный вариант/любой/вектор или похожий тип.
Если вы действительно хотите переадресовать объект, найдите реализацию std::optional
и создайте/уничтожьте объекты, используя это; он осторожен, и вы почти наверняка не будете достаточно осторожны.
Ответ 2
Это не очень хорошая идея, потому что вы все равно можете запустить деструктор дважды, если конструктор нового объекта выдает исключение. То есть деструктор всегда будет работать в конце области действия, даже если вы исключите область действия.
Вот пример программы, которая демонстрирует это поведение (Идеальная ссылка):
#include <iostream>
#include <stdexcept>
using namespace std;
struct Foo
{
Foo(bool should_throw) {
if(should_throw)
throw std::logic_error("Constructor failed");
cout << "Constructed at " << this << endl;
}
~Foo() {
cout << "Destroyed at " << this << endl;
}
};
void double_free_anyway()
{
Foo f(false);
f.~Foo();
// This constructor will throw, so the object is not considered constructed.
new (&f) Foo(true);
// The compiler re-destroys the old value at the end of the scope.
}
int main() {
try {
double_free_anyway();
} catch(std::logic_error& e) {
cout << "Error: " << e.what();
}
}
Отпечатки:
Построено в 0x7fff41ebf03f
Разрушен в 0x7fff41ebf03f
Разрушен в 0x7fff41ebf03f
Ошибка: сбой конструктора