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

Разрешено ли явно вызвать деструктор, за которым следует размещение 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.

4b9b3361

Ответ 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

Ошибка: сбой конструктора