Virtual destructor and memory deallocation
Asked Answered
M

3

3

I'm not quite sure I understand virtual destructors and the concept of allocating space on the heap right. Let's look at the following example:

class Base
{
public:
    int a;
};

class Derived : public Base
{
public:
    int b;
};

I imagine that if I do something like this

Base *o = new Derived;

that 8 Bytes (or whatever two integers need on the system) are allocated on the heap, which looks then something like this: ... | a | b | ...

Now if I do this:

delete o;

How does 'delete' know, which type o is in reality in order to remove everything from the heap? I'd imagine that it has to assume that it is of type Base and therefore only deletes a from the heap (since it can't be sure whether b belongs to the object o): ... | b | ...

b would then remain on the heap and be unaccessible.

Does the following:

Base *o = new Derived;
delete o;

truly provoke memory leaks and do I need a virtual destructor here? Or does delete know that o is actually of the Derived class, not of the Base class? And if so, how does that work?

Thanks guys. :)

Montalvo answered 4/8, 2013 at 20:6 Comment(4)
+1 Good question, but o should be a pointer, as in Base* o.Intersect
Your delete invokes undefined behavior.Lassiter
@LuchianGrigore Which is the only correct answer. You're usually pretty quick to answer---you should have done so this time, since you're correct, and the two answers I saw before I posted essentially the same thing were incorrect. (And your comment is really all you need to know.)Inflammable
@JamesKanze ah, my FGITW days are gone :)Lassiter
I
4

You're making a lot of assumptions about the implementation, which may or may not hold. In a delete expression, the dynamic type must be the same as the static type, unless the static type has a virtual destructor. Otherwise, it is undefined behavior. Period. That's really all you have to know—I've used with implementations where it would crash otherwise, at least in certain cases; and I've used implementations where doing this would corrupt the free space arena, so that the code would crash sometime later, in a totally unrelated piece of code. (For the record, VC++ and g++ both fall in the second case, at least when compiled with the usual options for released code.)

Inflammable answered 4/8, 2013 at 20:39 Comment(2)
So my code provokes undefined behaviour. But what can I do against that? Is it better to a) find out (e.g. based on a flag) what the type of the pointer is and cast it right before deleting or b) let Base have a virtual destructor (if so: does Derived need an explicit destructor or can I leave it as it is?)Montalvo
@Montalvo A lot depends on why you are inheriting. In some cases, the "error" is having a pointer to base to begin with. (Think of std::exception.) But for the usual cases, where inheritance is used to implement polymorphism, the base class should have a virtual destructor.Inflammable
S
2

There is no problem in the size of the object being deleted - it is known. The problem solved by virtual destructors can be demonstrated as follows:

class Base
{
public:
    Base() { x = new char[1]; }
    /*virtual*/ ~Base() { delete [] x; }

private:
    char* x;
};

class Derived : public Base
{
public:
    Derived() { y = new char[1]; }
    ~Derived() { delete [] y;}
private:
    char* y;
};

Then having:

Derived* d = new Derived();
Base* b = new Derived();

delete d;   // OK
delete b;   // will only call Base::~Base, and not Derived::~Derived

The second delete will not finalize the object properly. If the virtual keyword was uncommented, then the second delete statement will behave as expected, and it will call Derived::~Derived along with Base::~Base.

As pointed out in the comments, to be strict, the second delete yields an undefined behavior, but it's used here only for the sake of making the point about virtual destructors.

Shipper answered 4/8, 2013 at 20:13 Comment(0)
M
2

Firstly, the classes you declared in your example have trivial internal structure. From purely practical point of view, in order to destroy object of such classes properly the run-time code does not need to know the actual type of the object being deleted. All it needs to know is the proper size of the memory block to be deallocated. This is actually something that is already achieved by C-style library functions like malloc and free. As you probably know, free implicitly "knows" how much memory to deallocate. Your example above does not involve anything in addition to of that. In other words, your example above is not elaborate enough to truly illustrate anything C++-specific.

However, formally the behavior of your examples is undefined, since virtual destructor is formally required by C++ language for polymorphic deletion regardless of how trivial the internal structure of the class is. So, your "how delete knows..." question simply does not apply. Your code is broken. It does not work.

Secondly, the actual tangible C++-specific effects begin to appear when you begin to require non-trivial destruction for your classes: either by defining an explicit body for the destructor or by adding non-trivial member subobjects to your class. For example, if you add a std::vector member to your derived class, the destructor of the derived class will become responsible for (implicit) destruction of that subobject. And in order for that to work, you will have to declare you destructors virtual. A proper virtual destructor is called through the same mechanism as any other virtual function is called. That's basically the answer to your question: the run-time code does not care about the actual type of the object simply because the ordinary virtual dispatch mechanism will ensure that the proper destructor is called (just like it works with any other virtual function).

Thirdly, another significant effect of virtual destruction appears when you define dedicated operator delete functions for your classes. The language specification requires that the proper operator delete function is selected as if it is looked up from inside the destructor of the class being deleted. And many implementations implement this requirement literally: they actually implicitly call operator delete from inside the class destructor. In order for that mechanism to work properly, the destructor has to be virtual.

Fourthly, a part of your question seems to suggest that you believe that failing to define a virtual destructor will lead to "memory leaks". This a popular, but completely incorrect and totally useless urban legend, perpetuated by low-quality sources. Performing polymorphic deletion on a class that has no virtual destructor leads to undefined behavior and to completely unpredictable devastating consequences, not to some "memory leaks". "Memory leaks" are not the issue in such cases.

Michellmichella answered 4/8, 2013 at 20:24 Comment(4)
Deleting a derived object through a pointer to base is undefined behavoir is the destructor of base is not virtual. That's really all that has to be said. In practice, the real problems start showing up with multiple inheritance, where the wrong pointer will be passed to operator delete (but that's just an artifact of the way most compilers lay out classes---you could just as easily have the same problems with single inheritance).Inflammable
@JamesKanze Are you sure this is true for trivial and standard-layout classes (which is the case here)? No matter how compiler wants to layout, it has to obey C-struct layout for these types of classes. And in this case compiler HAS to make Base part in the beginning of the Derived object.Invent
@PetrBudnik Yes. And C-struct layout has nothing to say about derived classes. §5.3.5/3 "In the first alternative (delete object), if the static type of the operand is different from its dynamic type, the static type shall be a base class of the operand’s dynamic type and the static type shall have a virtual destructor or the behavior is undefined." You can't get much clearer.Inflammable
@JamesKanze Thank you. The quote from Standard was appreciated.Invent

© 2022 - 2024 — McMap. All rights reserved.