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

Переопределить оператор удаления с пустой реализацией

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

Если переопределить оператор delete в классе, дающем ему пустую реализацию {}, деструктор все равно будет вызываться, но память не освободится.

Предполагая, что деструктор также пуст, будет ли эффект delete иметь какой-либо эффект или было бы безопасно продолжать использовать "удаленный" объект (т.е. существует ли поведение undefined)?

struct Foo {
    static void operator delete(void* ptr) {}
    Foo() {}
    ~Foo() {}
    void doSomething() { ... }
}

int main() {
    Foo* foo = new Foo();
    delete foo;
    foo->doSomething(); // safe?
}

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

Обновление

Ссылаясь на некоторые ответы, в которых упоминаются утечки памяти: допустим, что перегруженный оператор delete не пуст, но сохраняет его аргумент ptr в (скажем, статический, для простоты) set:

struct Foo {
    static std::unordered_set<void*> deletedFoos;
    static void operator delete(void* ptr) {
        deletedFoos.insert(ptr);
    }
    Foo() {}
    ~Foo() {}
}

И этот set периодически очищается:

for (void* ptr : Foo::deletedFoos) {
    ::operator delete(ptr);
}
Foo::deletedFoos.clear();
4b9b3361

Ответ 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 в этом случае, но удаление и добавление его изменяет поведение компилятора, которое, я считаю, является артефактом реализации.

Ответ 2

Из N4296 (~ С++ 14):

3.8 Время жизни объекта [basic.life]

...

Время жизни объекта типа T заканчивается, когда:

(1.3) - если T - тип класса с нетривиальным деструктором (12.4), начинается вызов деструктора или

(1.4) - хранилище, которое объект занимает, повторно используется или освобождается.

Тогда:

12.4 Деструкторы [class.dtor]

...

Деструктор тривиален, если он не предоставляется пользователем, и если:

(5.4) - деструктор не virtual,

(5.5) - все прямые базовые классы его класса имеют тривиальные деструкторы, а

(5.6) - для всех нестатических членов данных своего класса, которые относятся к типу класса (или его массиву), каждый такой класс имеет тривиальный деструктор.

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

В принципе, для достаточно простых классов это безопасно, но если у вас есть какие-то классы владения ресурсами, это не будет законным.

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

Ответ 3

Может ли ваш хак работать, зависит от членов класса. Деструктор всегда вызывает деструкторы членов класса. Если у вас есть какие-либо члены, которые являются строками, векторами или другими объектами с "активным" деструктором, эти объекты будут уничтожены, даже жестко выделена память, выделенная для содержащего объекта.

Ответ 4

Основная проблема заключается в том, что даже если у них есть пустые dtor, будут происходить другие фоновые вещи, такие как выпуск таблиц виртуальных методов. Мой личный подход в таких случаях заключается в создании частного dtor и использовании метода члена (имя, которое он уничтожает, если хотите) для фильтрации оператора удаления. Например

class A
{
     bool _may_be_deleted;

public:

    A(bool may_be_deleted)
    : _may_be_deleted(may_be_deleted){;}

    void allow_delete()
    {
        _prevent_delete = false;
    }

    static bool destroy(A*);

private:

    virtual ~A(){;}
};

bool A::destroy(A *pA)
{
    if(pA->_may_be_deleted)
    {
        delete pA;
        return true;
    }
    return false;
}

int main(int argc, char* argv[])
{
    A* pA = new A(false);
    A::destroy(pA);     //returns false and A is not deleted
    pA->allow_delete();
    A::destroy(pA);     //Ok, now A is destroyed and returns true;
}

Надеюсь, что это поможет.