Ответ 1
Из n4296:
Деструктор вызывается неявно
(11.1) - для построенного объекта со статическим временем хранения (3.7.1) при завершении программы (3.6.3),
(11.2) - для построенного объекта с длительностью хранения потоков (3.7.2) при выходе потока,
(11.3) - для построенного объекта с автоматической продолжительностью хранения (3.7.3), когда блок, в котором создается объект, выходит (6.7),
(11.4) - для созданного временного объекта, когда его время жизни заканчивается (12.2).
В каждом случае контекст вызова - это контекст построение объекта. Деструктор также вызывается неявным образом посредством использования выражения-удаления (5.3.5) для построенного объекта выделенных новым выражением (5.3.4); контекст вызова является удалением-выражением. [Примечание: массив типа класса содержит несколько подобъектов, для каждого из которых вызывается деструктор. -конец note] Деструктор также может быть вызван явно.
Таким образом, само использование выражения delete, которое вызывает оператор delete, вы также неявно называете деструктор. Жизнь объекта закончилась, это поведение undefined, что произойдет, если вы вызовете метод для этого объекта.
#include <iostream>
struct Foo {
static void operator delete(void* ptr) {}
Foo() {}
~Foo() { std::cout << "Destructor called\n"; }
void doSomething() { std::cout << __PRETTY_FUNCTION__ << " called\n"; }
};
int main() {
Foo* foo = new Foo();
delete foo;
foo->doSomething();
// safe? No, an UB. Object life is ended by delete expression.
}
Вывод:
Destructor called
void Foo::doSomething() called
: gcc HEAD 8.0.0 20170809 с -O2
Вопрос начинается с предположения, что переопределение операции удаления и поведения объекта будет означать уничтожение объекта. Переопределение деструктора самого объекта не будет переопределять деструкторы его полей. На самом деле он больше не будет существовать с точки зрения семантики. Он не будет освобождать память, что может случиться, если объект хранится в пуле памяти. Но он удалит абстрактную "душу" объекта, так сказать. Вызов методов или доступ к полям объекта после этого - UB. В частном случае, в зависимости от операционной системы, эта память может оставаться навсегда распределенной. Это небезопасное поведение. Также небезопасно предполагать, что компилятор будет генерировать разумный код. Он вообще может опускать действия.
Позвольте мне добавить некоторые данные к объекту:
struct Foo {
int a;
static void operator delete(void* ptr) {}
Foo(): a(5) {}
~Foo() { std::cout << "Destructor called\n"; }
void doSomething() { std::cout << __PRETTY_FUNCTION__ << "a = " << a << " called\n"; }
};
int main() {
Foo* foo = new Foo();
delete foo;
foo->doSomething(); // safe?
}
Вывод:
Destructor called
void Foo::doSomething() a= 566406056 called
Hm? Мы не инициализировали память? Позвольте добавить один и тот же вызов перед уничтожением.
int main() {
Foo* foo = new Foo();
foo->doSomething(); // safe!
delete foo;
foo->doSomething(); // safe?
}
Вывод здесь:
void Foo::doSomething() a= 5 called
Destructor called
void Foo::doSomething() a= 5 called
Что? Конечно, компилятор просто пропустил инициализацию a в первом случае. Может быть, потому, что класс ничего не делает? В этом случае это возможно. Но это:
struct Foo {
int a, b;
static void operator delete(void* ptr) {}
Foo(): a(5), b(10) {}
~Foo() { std::cout << "Destructor called\n"; }
void doSomething() { std::cout << __PRETTY_FUNCTION__ << " a= " << a << " called\n"; }
};
int main() {
Foo* foo = new Foo();
std::cout << __PRETTY_FUNCTION__ << " b= " << foo->b << "\n";
delete foo;
foo->doSomething(); // safe?
}
будет генерировать аналогичное значение undefined:
int main() b= 10
Destructor called
void Foo::doSomething() a= 2017741736 called
Компилятор считал поле a
неиспользованным к моменту смерти foo
и, таким образом, "мертвым", не влияя на дальнейший код. foo
опустился со всеми "руками", и никто из них официально не существует. Не говоря уже о том, что в Windows, используя компилятор MS, эти программы, скорее всего, сбой, когда Foo::doSomething()
попытается оживить мертвого члена. Размещение нового позволило бы нам сыграть роль доктора Франкенштейна:
#include <iostream>
#include <new>
struct Foo {
int a;
static void operator delete(void* ptr) {}
Foo() {std::cout << __PRETTY_FUNCTION__ << " a= " << a << " called\n"; }
Foo(int _a): a(_a) {std::cout << __PRETTY_FUNCTION__ << " a= " << a << " called\n"; }
~Foo() { std::cout << "Destructor called\n"; }
void doSomething() { std::cout << __PRETTY_FUNCTION__ << " a= " << a << " called\n"; }
};
int main() {
Foo* foo = new Foo(5);
foo->~Foo();
Foo *revenant = new(foo) Foo();
revenant->doSomething();
}
Вывод:
Foo::Foo(int) a= 5 called
Destructor called
Foo::Foo() a= 1873730472 called
void Foo::doSomething() a= 1873730472 called
Безрезультатно, если мы будем называть деструктор или нет, компиляторам разрешено решать, что ревант не то же самое, что и исходный объект, поэтому мы не можем повторно использовать старые данные, а только выделенную память.
Любопытно, что при выполнении UB, если мы удалим оператор delete из foo
, эта операция, похоже, работает с GCC. Мы не вызываем delete в этом случае, но удаление и добавление его изменяет поведение компилятора, которое, я считаю, является артефактом реализации.