Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Override delete operator with empty implementation

Tags:

c++

Using the delete operator on an object normally leads to two things: calling the object's destructor (and its virtual base destructors, if present) and freeing the memory afterwards.

If override the delete operator on a class giving it an empty implementation {}, the destructor will still be called, but the memory does not get freed.

Assuming the destructor is also empty, will the delete then have any effect or would it be safe to continue using the "deleted" object (i.e. is there undefined behaviour)?

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

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

Not that this would make much sense as it is, but I'm investigating in a "deferred delete" (gc) mechanism where objects won't get deleted instantly when delete gets called but shortly afterwards.

Update

Referring to some answers that mention memory leaks: let's assume the overloaded delete operator is not empty, but does store its ptr argument in a (let's say static, for the sake of simplicity) set:

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

And this set gets cleaned up periodically:

for (void* ptr : Foo::deletedFoos) {
    ::operator delete(ptr);
}
Foo::deletedFoos.clear();
like image 203
csk Avatar asked Aug 11 '17 06:08

csk


People also ask

Can we overload new and delete operator in C++?

Overloading New and Delete operator in c++ The new and delete operators can also be overloaded like other operators in C++. New and Delete operators can be overloaded globally or they can be overloaded for specific classes.

When to use delete [] vs delete C++?

delete is used for one single pointer and delete[] is used for deleting an array through a pointer.

Does delete operator deallocates memory?

Using the delete operator on an object deallocates its memory. A program that dereferences a pointer after the object is deleted can have unpredictable results or crash.

Why overload new and delete?

The most common reason to overload new and delete are simply to check for memory leaks, and memory usage stats. Note that "memory leak" is usually generalized to memory errors. You can check for things such as double deletes and buffer overruns.


3 Answers

From n4296:

A destructor is invoked implicitly

(11.1) — for a constructed object with static storage duration (3.7.1) at program termination (3.6.3),

(11.2) — for a constructed object with thread storage duration (3.7.2) at thread exit,

(11.3) — for a constructed object with automatic storage duration (3.7.3) when the block in which an object is created exits (6.7),

(11.4) — for a constructed temporary object when its lifetime ends (12.2).

In each case, the context of the invocation is the context of the construction of the object. A destructor is also invoked implicitly through use of a delete-expression (5.3.5) for a constructed object allocated by a new-expression (5.3.4); the context of the invocation is the delete-expression. [ Note: An array of class type contains several subobjects for each of which the destructor is invoked. —end note ] A destructor can also be invoked explicitly.

Thus, the very use of delete expression that calls delete operator, you implicitly call destructor as well. Object's life ended, it's an undefined behavior what happens if you will call a method for that object.

#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's life is ended by delete expression.
}

Output:

Destructor called
void Foo::doSomething() called

used: gcc HEAD 8.0.0 20170809 with -O2

The question starts with assumption that redefining delete operator and behaviour of object would omit destruction of object. Redefining destructor of object itself will not redefine destructors of its fields. In fact it won't exist anymore from semantics point of view. It will not deallocate memory, which might be a thing if object is stored in memory pool. But it would delete abstract 'soul' of object, so to say. Calling methods or accessing fields of object after that is UB. In particular case, depending on operation system, that memory may stay forever allocated. Which is an unsafe behavior. It also unsafe to assume that compiler would generate sensible code. It may omit actions altogether.

Let me add some data to object:

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?
}

Output:

Destructor called
void Foo::doSomething() a= 566406056 called

Hm? We didn't initialized memory? Let's add same call before destruction.

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

Output here:

void Foo::doSomething() a= 5 called
Destructor called
void Foo::doSomething() a= 5 called

What? Of course, compiler just omitted initialization of a in first case. Could it be because class doesn't do anything else? In this case it is possible. But this:

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?
}

will generate similar undefined value:

int main() b= 10
Destructor called
void Foo::doSomething() a= 2017741736 called

Compiler had considered field a unused by the time of death of foo and thus "dead" without impact on further code. foo went down with all "hands" and none of them formally do exist anymore. Not to mention that on Windows, using MS compiler those programs would likely crash when Foo::doSomething() would try to revive the dead member. Placement new would allow us to play Dr.Frankenstein role:

    #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(); 
}

Output:

Foo::Foo(int) a= 5 called
Destructor called
Foo::Foo() a= 1873730472 called
void Foo::doSomething() a= 1873730472 called

Irregardless to wit if we call destructor or not, compilers are allowed to decide that revenant isn't same thing as original object, so we can't reuse old data, only allocated memory.

Curiously enough, while still performing UB, if we remove delete operator from Foo, that operation seem to work as expected with GCC. We do not call delete in this case, yet removal and addition of it changes compiler behavior, which, I believe, is an artifact of implementation.

like image 157
Swift - Friday Pie Avatar answered Oct 14 '22 14:10

Swift - Friday Pie


From N4296 (~C++14):

3.8 Object lifetime [basic.life]

...

The lifetime of an object of type T ends when:

(1.3) — if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or

(1.4) — the storage which the object occupies is reused or released.

Then:

12.4 Destructors [class.dtor]

...

A destructor is trivial if it is not user-provided and if:

(5.4) — the destructor is not virtual,

(5.5) — all of the direct base classes of its class have trivial destructors, and

(5.6) — for all of the non-static data members of its class that are of class type (or array thereof), each such class has a trivial destructor.

Otherwise, the destructor is non-trivial.

So basically, for simple enough classes, this is safe, but if you have any kind of resource-owning classes involved it is not going to be legal.

Take care, though, that in your example your destructor is user-provided, hence non-trivial.

like image 31
BoBTFish Avatar answered Oct 14 '22 14:10

BoBTFish


Whether your hack can work depends on the members of the class. The destructor will always call the destructors of the class members. If you have any members that are strings, vectors, or other objects with an 'active' destructor, these objects will be destroyed, even tough the memory allocated for the containing object is still allocated.

like image 24
Michaël Roy Avatar answered Oct 14 '22 14:10

Michaël Roy