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