Path: utzoo!utgpu!jarvis.csri.toronto.edu!rutgers!ukma!tut.cis.ohio-state.edu!SUN.COM!tiemann From: tiemann@SUN.COM (Michael Tiemann) Newsgroups: gnu.g++ Subject: Destructor called too soon, Destructor not called - G++1.35 Message-ID: <8909040747.AA00164@teacake.sun.com> Date: 4 Sep 89 07:47:33 GMT References: Sender: daemon@tut.cis.ohio-state.edu Reply-To: tiemann@lurch.stanford.edu Distribution: gnu Organization: GNUs Not Usenet Lines: 172 This is a great example of C++ abuse. It shows how different C and C++ can be. My comments appear as //** (apologies to anyone without the GNU C preprocessor :-) Please do not take my comments as being derogatory towards Andrew or his code. I am just using this code because it serves as such a good example. When I refer to `you', I refer to the general second person of the C++ programming community. Date: 3 Aug 89 04:11:06 GMT From: watmath!watcgl!andrewt@uunet.uu.net (Andrew Thomas) Organization: University of Waterloo, Waterloo, Ontario, Canada Sender: bug-g++-request@prep.ai.mit.edu System: uVax II , Ultrix 2.0 G++ 1.35 Problem: The destructors for class instances passed as return values (rather than returning a pointer or reference) get called too soon if only a member of the class is of interest. Also, sometimes no destructor is called at all. Program: The following program exhibits both behaviours described above: ----------------- cut ---------------------------- //** Inclusion of probably for luck--see below. #include //** A perfectly good C++ class class String { char *_ptr; public: String (String&); String (char *); ~String(); char* ptr() { return (_ptr); } }; //** A perfectly good C++ copy-constructor String::String (String& old) { char* a = new char[strlen(old._ptr) + 1]; strcpy (a, old._ptr); _ptr = a; printf ("Constructing: %s (%x)\n", a, a); } //** A perfectly good C++ initialization-constructor String::String (char* old) { char* a = new char[strlen(old) + 1]; strcpy (a, old); _ptr = a; printf ("Constructing: %s (%x)\n", a, a); } //** A perfectly good C++ destructor String::~String() { printf ("deleting %s (%x)\n", _ptr, _ptr); delete _ptr; } //** A perfectly good C++ value-producing function String my_func () { String x = "Hello"; return (x); } //** Now here's where all hell breaks loose... main () { //** Here printf is used, in spite of the fact that //** was #included. If this went through //** the stream interface, attempts to `print' a String //** would be caught as compile-time errors. Instead, //** the go into the `...' black-hole of printf... printf ("Function: %s (%x)\n", my_func(), my_func()); //** Here we get lucky: the return type of String::ptr () is //** `char *', which is a builtin C type, so the call to //** printf succeeds. printf ("Function: %s (%x)\n", my_func().ptr(), my_func().ptr()); } ------------------------- cut ------------------------- The output of this program looks like: //** Note!! This is on a VAX. On a Sun4, this program core-dumps. //** Explanation below. Constructing: Hello (1c00) Constructing: Hello (1c10) deleting Hello (1c00) Constructing: Hello (1c00) Constructing: Hello (1c20) deleting Hello (1c00) Function: Hello (1c10) Constructing: Hello (1c00) Constructing: Hello (1c30) deleting Hello (1c00) deleting Hello (1c30) Constructing: Hello (1c30) Constructing: Hello (1c00) deleting Hello (1c30) deleting Hello (1c00) Function: Hello (1c30) Note only 6 destructors, and 8 constructors. Also note that memory address 1c30 is deleted with an automatically called destructor before the String instance is actually used. //** You have to look at the assembly code to actually make that //** statement. In the code I looked at, everything is as it should //** be. It's been a year since I said this, but it's still true: the //** most difficult way to observe code is from the behavior of a //** program, because under those conditions, you must make //** assumptions and interpretations. Slightly easier, though still //** difficult and subject to assumptions it to look at output from a //** C++ to C translator, such as cfront. For a really good //** understanding of what is going on, the assembly code is really //** where its at. -- Andrew Thomas andrewt@watsnew.waterloo.edu Systems Design Eng. University of Waterloo "If a million people do a stupid thing, it's still a stupid thing." - Opus Ok. Here is why the program will never work under GNU C++. If you have a function which takes an aggregate parameter that has a destructor, there are two places where the object could be destroyed. One place is in the scope of the caller, the other in the scope of the callee. In the case of the former, the aggregate parameter cannot be passed in the stack. Instead, a local temporary must be allocated, and a reference to that temporary passed to the callee. If space in the stack is used to hold the object, it could get clobbered when the callee returns, since the stack space could be deallocated by the return statement, and the caller has no control over that. Therefore, if there are N calls to this function, then there must be N places where cleanup code is emitted. In the case of the latter, one only needs to have the cleanup code extant in one location: the called function. The callee's stack frame remains allocated until the callee returns, so arguments can be passed *in* the stack, rather than just passing references to the caller's frame. This is more efficient in both time (one fewer indirection), and space (only one site for cleanup code). And it compiles faster. Cfront implements the first mechanism, while GNU C++ implements the second. The 2.0 reference manual states that either is an acceptable way of doing business. HOWEVER, if you do it the GNU C++ way, you must be honest! Namely, if you pass an object (not a reference to an object) to a function, then such passing must be done type-correctly. You can pass it via varargs, but that function must be prepared to call a destructor by hand, just as it pulls arguments from the argument list by hand. When you pass objects in the stack to printf, then tell printf `print a %s', printf may expect that the argument came in a register (such as on my Sun4), when in fact it came on the stack (such as a String object must). You won't necessarily notice a problem on a machine that passes both built-in types and objects on the stack (such as a VAX), but that does not mean a problem does not exist. The reason that two destructors were missed is because `printf' was responsible for calling them, and didn't. Hope this was useful... Michael