[comp.lang.c++] destructors, derived classes, and pointers

pete@violet.berkeley.edu (Pete Goodeve) (10/30/88)

As an eager new user of C++ (on the Amiga), a few days ago I posted a fairly
enthusiastic review of Lattice C++ to comp.sys.amiga.  Unfortunately my
first attempt to use it for serious work has caused a quick degradation in
some of the gloss. In fact it looks as if the tarnish may run fairly deep
into the language; I doubt if it's Lattice's fault, as they are paying a
fairly hefty fee to AT&T for their direct port of cfront.

My goal in the project where I ran into trouble is briefly this:  I'll be
managing linked lists of objects, which will be all derived from a common
"Object" base class, but may have widely differing storage requirements and
structures.  When an object is unlinked from a list -- by one common
procedure which will handle all the different types -- it will have to be
deleted.  This in turn means disposing of any data blocks, lists, and so on
that may be attached to the object.  Well this ought to be easy, right?  I
mean, this is exactly what constructors and destructors are designed for...

The problem is that all the managing procedure has is a pointer to the
object (of the base type naturally).  Now I would expect that a delete call
on this pointer would detect the correct destructor to invoke before it
actually disposed of the memory, but NOOO!  It calls ONLY the parent type
destructor!!!  So much for nice neat cleanup...

Fortunately I can work around by supplying a truly virtual "Cleanup"
function for each object type, and arrange that the parent destructor calls
this.  This works, but in my investigations, I found that the whole
construction/destruction business seems royally screwed up when derived
classes and pointers are involved.  The best thing, I think, is to take a
look at the test program I ended up with...

                            ++ ++ ++

// constructor -- destructor test

#include <stream.h>
// (though I mostly use printf...)

/*** a base class to start with: ***/
// (I just used structures for simplicity)
struct parent {
    int i;
    parent(int val=0);
    ~parent();
    virtual void Begin();
    virtual void End();
};


/*** Constructor and Destructor for parent: ***/

parent::parent(int val=0)
{
    i = val;
    printf("   parent constructor %d [%x] // ", this->i, this);
    this->Begin(); // SHOULD call the virtual function for the actual class
}

parent::~parent()
{
    printf("   parent destructor %d [%x]  // ", this->i, this);
    this->End(); // virtual again...
}


/*** Parent level virtual functions: ***/

void parent::Begin()
{
    printf("parent Begin %d [%x]\n", this->i, this);
}

void parent::End()
{
    printf("parent End %d [%x]\n", this->i, this);
}


/*** Then the derived structure: ***/

struct derived : parent {
    int j;
    derived(int val);
    ~derived();
    void Begin();
    void End();
};


/*** Constructor and Destructor for derived class: ***/

derived::derived(int val) : (val)
{
    printf("   derived constructor %d [%x] // ", this->i, this);
    this->Begin();
}

derived::~derived()
{
    printf("   derived destructor %d [%x] // ", this->i, this);
    this->End();
}

/*** virtual derived functions: ***/

void derived::Begin()
{
    j = 55;
    printf("derived Begin %d [%x]\n", this->i, this);
}

void derived::End()
{
    printf("derived End %d [%x]\n", this->i, this);
}

/***************************************************/

main()
{
    int line = 0;
    printf("\n(%d) creating parent object (and pointer)..\n", ++line);
    parent test(11), *pp;
    printf("\n(%d) creating derived object (and pointer)..\n", ++line);
    derived junk(22), *dp;
    printf("\n(%d) assigning derived object to parent pointer..\n", ++line);
    pp = new derived(33);
    printf("\n(%d) assigning derived object to derived pointer..\n", ++line);
    dp = new derived(44);
    printf("\n(%d) output contents:\n", ++line);
    cout << "   " << test.i << "\n";
    cout << "   " << junk.i << "," << junk.j << "\n";
    cout << "   " << pp->i << "\n";
    cout << "   " << dp->i << "," << dp->j << "\n";
    printf("\n(%d) deleting derived via parent pointer..\n", ++line);
    delete pp;
    printf("\n(%d) deleting derived via derived pointer..\n", ++line);
    delete dp;
    printf("\n(%d) end of main\n", ++line);
}

                            ++ ++ ++


The output from this program is...

                            .........

(1) creating parent object (and pointer)..
   parent constructor 11 [268fec] // parent Begin 11 [268fec]

(2) creating derived object (and pointer)..
   parent constructor 22 [268fdc] // parent Begin 22 [268fdc]
   derived constructor 22 [268fdc] // derived Begin 22 [268fdc]

(3) assigning derived object to parent pointer..
   parent constructor 33 [23de00] // parent Begin 33 [23de00]
   derived constructor 33 [23de00] // derived Begin 33 [23de00]

(4) assigning derived object to derived pointer..
   parent constructor 44 [23de10] // parent Begin 44 [23de10]
   derived constructor 44 [23de10] // derived Begin 44 [23de10]

(5) output contents:
   11
   22,55
   33
   44,55

(6) deleting derived via parent pointer..
   parent destructor 33 [23de00]  // derived End 33 [23de00]

(7) deleting derived via derived pointer..
   derived destructor 44 [23de10] // derived End 44 [23de10]
   parent destructor 44 [23de10]  // derived End 44 [23de10]

(8) end of main
   derived destructor 22 [268fdc] // derived End 22 [268fdc]
   parent destructor 22 [268fdc]  // derived End 22 [268fdc]
   parent destructor 11 [268fec]  // parent End 11 [268fec]

                            .........

Referring to the numbered sections, (1) looks OK -- the parent class object
was created with a parent constructor, which in turn called the parent's
virtual "Begin".

In (2) we see the creation of a derived object.  It looks OK too at first:
the parent constructor is invoked, then the derived class one.  But, hold
it!  "Begin" is a VIRTUAL function isn't it?  So why did the parent
constructor call the PARENT "Begin" rather than the derived one that
corresponds to the type of the object it just created??

(3) and (4) -- where we assign derived objects to pointers -- behave
exactly the same.

In (6) we delete the derived object that is only known by it's parent
pointer.  As I said at the beginning, this situation was the cause of all
my original grief.  Only the PARENT destructor is called, but here I have
made it work because the virtual "End" function is called correctly this
time.

In (7) the delete is performed on a pointer of the correct type, and
things work as we ought to expect.  Both destructors are invoked, and they
both call the derived "End" fuction (correctly).

In (8) we see the automatic cleanup being performed on the local objects,
and here again things do work correctly.  The right destructors and "End"
functions are called.

                            ++ ++ ++

So, am I right that this is a general problem with C++ ?  What worries me
most is the thought that there may be other similar inconsistencies in the
fabric of the compiler that will make it hard to be sure one's program will
behave correctly under all circumstances.  Overall I like the features of
the language; also I realize that it's still evolving.  However, I'd hate
to always have that nagging lack of trust...

                                                -- Pete --

ark@alice.UUCP (Andrew Koenig) (10/31/88)

In article <16219@agate.BERKELEY.EDU>, pete@violet.berkeley.edu (Pete Goodeve) writes:

> The problem is that all the managing procedure has is a pointer to the
> object (of the base type naturally).  Now I would expect that a delete call
> on this pointer would detect the correct destructor to invoke before it
> actually disposed of the memory, but NOOO!  It calls ONLY the parent type
> destructor!!!  So much for nice neat cleanup...

The solution to your problem is to declare the destructor as
virtual in the base class.  For more detail, see my article
``An example of dynamic binding in C++'' in the Journal of
Object-Oriented Programming, vol. 1, #3.

Example of the syntax:

	class Node: {
	// stuff
	public:
		Node();
		virtual ~Node();
	// more stuff
	};

	class SpecialNode: public Node {
	// stuff
	public:
		SpecialNode();
		~SpecialNode();
	// more stuff
	};

You only need to make the destructor virtual in the base class.
You don't -- and indeed, can't -- make any constructors virtual.

Caveat: some versions of the C++ translator have a bug
that doesn't get things quite right unless every derived class
has an explicit destructor.  To avoid getting bitten by this bug,
write a destructor even if you don't need it:

	class NodeWithEmptyDestructor: public Node {
	// stuff
	public:
		~NodeWithEmptyDestructor() { }
	};
-- 
				--Andrew Koenig
				  ark@europa.att.com

mball@cod.NOSC.MIL (Michael S. Ball) (10/31/88)

In article <16219@agate.BERKELEY.EDU> pete@violet.berkeley.edu (Pete Goodeve) writes:
>The problem is that all the managing procedure has is a pointer to the
>object (of the base type naturally).  Now I would expect that a delete call
>on this pointer would detect the correct destructor to invoke before it
>actually disposed of the memory, but NOOO!  It calls ONLY the parent type
>destructor!!!  So much for nice neat cleanup...
>
Make your destructor virtual and all will work as you wish......

Mike Ball
TauMetric Corporation
1094 Cudahy Pl. Ste 302
San Diego, CA 92110
(619)275-6381

pete@violet.berkeley.edu (Pete Goodeve) (10/31/88)

Mike Ball [in <1285@cod.NOSC.MIL>] answers my screams of anguish:

> >The problem is that all the managing procedure has is a pointer to the
> >object (of the base type naturally).  Now I would expect that a delete call
> >on this pointer would detect the correct destructor to invoke before it
> >actually disposed of the memory, but NOOO!  It calls ONLY the parent type
> >destructor!!!  So much for nice neat cleanup...
> >
> Make your destructor virtual and all will work as you wish......

Hmmm. Yup, that works.  Thanks.

Actually I thought I'd tried that in the course of my rushing in five
directions at once...  I was probably fooled by the fact that virtual
CONstructors are illegal. Also my only reference at the moment is Wiener
and Pearson, who don't mention that possibility.  [It seems that our local
bookstores are out of Stroustrup's book at the moment.  Is that maybe a
good sign?  On the other hand, the UC Berkeley library doesn't have a
SINGLE copy!]

I'm still not entirely happy, because this solution seems particularly ad
hoc.  I don't like Ifs Ands and Buts in a language.  I can't see that you
would EVER want ONLY the parent class destructor to be invoked, so that
there should be some protection against this.  I realize that this is a
case where efficiency has taken precedence over security, but I'm not sure
this was a wise choice.  Could the compiler raise an error flag if you ever
assigned a derived object with a destructor to a pointer of the base class
WITHOUT a virtual destructor?  I think this might solve the dilemma.

As a final carp, there is still the other inconsistency I noted [section
(1) of my last message] in that a 'this->' reference in a parent
constructor picks up the PARENT's virtual function, rather than the derived
class function that would be invoked in any other case.  This surely is
wrong?
                                            -- Pete --