Topic: Finding the right operator new()/delete() (was A BIG bug...)


Author: rfg@netcom.com (Ronald F. Guilmette)
Date: Fri, 19 Nov 1993 09:37:57 GMT
Raw View
In article <CGAu8G.KD@ses.com> jamshid@ses.com (Jamshid Afshar) writes:
>In article <rfgCFt39y.Kyo@netcom.com>,
>Ronald F. Guilmette <rfg@netcom.com> wrote:
>>The point that is made at the end of 12.5 is that delete() functions are
>>*not* called where they ought to be called (i.e. where any sane person
>>would expect them to be called, at the point where you see a delete
>>statement) but rather, compilers are expected to generate implicit calls
>>to delete() functions WITHIN DESTRUCTORS.  That is plainly nonsensical,
>>and the results obtained from implementations which do this are plainly
>>nonsensical.
>
>First, you're confusing semantic requirements with common
>implementation techniques.  The ARM simply requires that when deleting
>an object that has a virtual destructor, the appropriate destructor
>and `operator delete()' are called for that object.  How a compiler
>calls the most-derived class' versions of these functions is up to the
>compiler writer.

Sorry Jamshid, but I don't think it's *me* who is confused.  Rather, I
think it's the implementations and the ARM.

Note that the ARM say EXPLICITLY at the end of 12.5 that "A destructor
finds the operator delete()...".  That statement is in a NON-COMMENTARY
part of the ARM, so it constitutes an actual "semantic requirement"
(using your terminology) and not merely a statement of one possible
implementation technique.  (And I note that the 9/28/93 X3J16 working
paper contains essentially similar wording.)

So much for the ARM.  Now, regarding the implementations...

In my previous message on this topic I gave an example of a short piece
of code which behaved oddly due to this "standard implementation hack" of
calling `new' (*implicitly* and without the programmer's consent) from
within a constructor and (similarly) calling delete (again, implicitly
and without the programmer's consent) from within a destructor.  I'll
now give an even more compelling example of this same problem.

Consider the following code:

 #include <stddef.h>

 struct B1 { void *operator new (size_t); };
 struct B2 { void *operator new (size_t); };

 struct D : public B1, public B2 { D(); };

 D::D () { }

More than a few existing C++ compilers actually *fail* to compile this
example, claiming that `operator new' is ambiguous.

But why?

Do you see any calls to `operator new' in this code??  Do you see any
use of the heap whatsoever??

Now you may claim that all of these implementors have merely botched the
job, and (as I do) you may claim that there is no excuse for compilers
to reject the above code, but given the nearly universal extent to which
this particular implementation botch has been mimicked (by implementors)
I have a STRONG suspicion that rather than requiring something sensible
(like for instance that compilers accept the above code) X3J16 (which,
after all, includes a rather large contingent of compiler implementors
and compiler vendors) is going to instead simply decide to condone this
bit of "existing practice" and thus officially sanction this exact kind
of implementation botch as "part of the definition of the language".

I feel so certain that X3J16 will, in the end, simply *standardize* this
awful language wart (rather than abolishing it) that I'm willing to take
bets on it.  Any takers?

--

-- Ronald F. Guilmette, Sunnyvale, California -------------------------------
------ domain address: rfg@netcom.com ---------------------------------------
------ uucp address: ...!uunet!netcom.com!rfg -------------------------------




Author: jamshid@ses.com (Jamshid Afshar)
Date: Wed, 10 Nov 1993 23:21:52 GMT
Raw View
In article <rfgCFt39y.Kyo@netcom.com>,
Ronald F. Guilmette <rfg@netcom.com> wrote:
>The point that is made at the end of 12.5 is that delete() functions are
>*not* called where they ought to be called (i.e. where any sane person
>would expect them to be called, at the point where you see a delete
>statement) but rather, compilers are expected to generate implicit calls
>to delete() functions WITHIN DESTRUCTORS.  That is plainly nonsensical,
>and the results obtained from implementations which do this are plainly
>nonsensical.

First, you're confusing semantic requirements with common
implementation techniques.  The ARM simply requires that when deleting
an object that has a virtual destructor, the appropriate destructor
and `operator delete()' are called for that object.  How a compiler
calls the most-derived class' versions of these functions is up to the
compiler writer.

Second, what's wrong with the common implementation technique of
calling `operator delete()' in virtual destructors?  It's better than
the only alternative I can think of: another vtable entry for the
`operator delete()' function.

I agree that there are bugs in some current implementations, though.
Compilers should accept your code below.  I can't find anything in the
ARM which in any way implies that your code is illegal nor is your
code difficult to implement.  g++ 2.5 and cfront 3.0.2 give errors but
Watcom C++ and Borland C++ 3.1 accept it without complaint.

>Let's say I have a particular class type `B' and that I wish to insure that
>no user of B is ever able to create or destroy a B out in the heap.  (This
>seems like a reasonable thing to want to do in certain circumstances.)  Now
>consider the following code, in which I insure this by giving B its own
>new and delete operators, and by making them private to B:
>
> #include <stddef.h>
>
> struct B {
>   private:
>     void *operator new (size_t);
>     void operator delete (void*);
>   public:
>     ~B () { }
> };
>
> struct D : public B { ~D () { } };
>
> void fubar () {
>    D d;  // error!!??
> }

This shouldn't be an error.  The fix that cfront and g++ need to
implement is very simple: do not check access in compiler-generated
calls to `operator delete()' in dtors.  This is in not dangerous
because the compiler will flag any use of the `delete' operator on
either a B* or a D* by anyone without private access to B.

Also, compilers can inline calls to the destructor and to `operator
delete()' in `delete' expressions involving pointers to objects whose
destructor is not virtual.  So, ~D() above doesn't even need to call
`operator delete()'.  If ~B() were virtual, though, it would need to
call `operator delete()' in order for the following to work:

 class B {
    void operator delete(void*);
 public:
    virtual ~B();
    friend void kill(B*);
 };

 class D : virtual public B {
 public:
    ~D();
 };

 D::~D() { cout << "destroying a D" << endl; }

 void kill(B* b) {
    delete b;  // should call ~D() which would call D::operator
               // delete() if it exists, otherwise B's
 }

 main() {
    kill( new D );
 }

Like virtual functions, access checking for `operator delete()' should
be done at the calling point (the `delete' expression) but the
most-derived class' function should be called.  You probably won't
like this, but as with virtual functions, most implementations will
require `B::operator delete()' be defined even if kill() doesn't
exist.

>Now you tell me...  If I am NEVER allocating or deallocating anything [...]
>But most implementations *do* gripe about the fact that the delete() operator
>is private?  Is this the most absurd nonsense you have ever seen in your
>life or what?  You be the judge.

Nah, the South (North?) Carolina woman losing custody of her child
because she is a lesbian is much higher on my list.  Actually, this
compiler bug does make sense in that I can see how it would be easy
for compilers to generate the `operator delete()' call (and other
hidden calls/variables/functions) exactly as if the code were written
by the user.

In any case, you should know better than to judge the correctness of
code by running it through current compilers (especially g++ and
cfront).

>I happen to think that this is absolutely the most blatant example of a
>case in which the current C++ language definition is absolutely, positively,
>and without doubt BADLY BROKEN and UTTERLY RIDICULOUS.

The langauge is not broken (at least in this respect).  Some compilers
are.

>The problem quite clearly is that calls to delete operators WHICH SHOULD
>BE GENERATED INLINE AT THE POINT OF A DELETE EXPRESSION are instead
>generated (implicitly) AS THE VERY LAST THING IN THE BODY OF EACH AND
>EVERY DESTRUCTOR... EVEN IN CASES WHERE THOSE CALLS WILL NEVER EVEN BE
>EXERCIZED.
>(This is not only patently ridiculous, it is potentially quite wasteful
>of code space also.)

Calls to `operator delete()' CANNOT be generated inline when deleting
through a pointer to an object which has a virtual destructor.  What
if the pointer points to a virtual base class?  The compiler can't
know at the `delete' expression what `void*' to pass to `operator
delete(void*)'.  How would it even know which `operator delete()' to
call?

I'm sure there are other possible implementation techniques.  The ARM
doesn't mandate the technique used by many current implementations,
but I don't know of any other technique that's more efficient.

>And while we are on the subject, let me also note that a perfectly
>analogous bit of nonsense also applies in the case of constructors and
>`new' operators.  Are calls to `new' operators generated where we would
>intutively expect them to be... i.e. at the points where we see `new'
>expressions?  No.  Of course not.  That would be too sensible.  Instead
>there are some mumbo-jumbo rules which (in effect) force implementations
>to do the same silly thing that cfront does, i.e. generate calls to
>`new' operators as the first thing within each constructor function.

Can you elaborate on the mumbo-jumbo rules?  I know that when using
virtual inheritance a flag has to be passed down to base constructors
that says "the virtual base has been constructed already, you don't
need to do it", but I don't know of anything stopping a compiler from
inlining all calls to the appropriate `operator new()' and ctor at the
`new' expression.  A simple test with `g++ -S' seems to show that the
call to `operator new()' is done at the `new' expression and that a
constructor contains no reference to any `operator new()'.  I believe
stone age C++ compilers had to call `operator new()' in the ctor to
allow assignment to this.

>So to summarize, the current rules with respect to construction & new
>and destruction & delete cause the normal accessability rules of the
>language to get totally bent (as illustrated by my example above).
>Additionally, the current way of doing things in this area is highly
>non-intutive, and is also quite inefficient, both in terms of space
>and speed.

Again, the language isn't broken.  Some current implementations are.
There's nothing in the language requiring nonsensical or inefficient
behavior when calling (or not calling) `operator new()' and `operator
delete()'.

Your ambigous  `operator delete()' example in another post did throw
me for a loop, but I think it's clear what ANSI C++ should require,
even if the ARM doesn't explicitly discuss it.

 struct Left { void operator delete(void*); };
 struct Right { void operator delete(void*); };
 struct Mix : public Left, public Right {};

There's no virtual destructors here so a compiler doesn't have to
generate any hidden calls to any `operator delete()' in ~Mix().  If
virtual destructors are used:

 struct Left { void operator delete(void*); virtual ~Left(); };
 struct Right { void operator delete(void*); virtual ~Right(); };
 struct Mix : public Left, public Right {};

 void kill( Mix* m, Left* l, Right* r) {
    delete m;  // ambiguity error
    delete l;  // okay, call ~Mix() and Left::operator delete()
    delete r;  // okay, call ~Mix() and Right::operator delete()
 }

I think this is actually easy to implement.  Mix has two vtables: one
for Left and one for Right.  The Left dtor vtable entry would point to
a function which destroys Mix members and calls ~Left() and ~Right().
It would then call `Left::operator delete()' if the dtor is called as
part of a delete expression (the compiler passes a hidden flag).  A
similar function would be generated for the Right dtor vtable entry.

The Mix dtor would destroy Mix members and bases, but it need not call
any `operator delete()' because it's impossible to have called this
dtor as part of a delete expression (the compiler would have given an
ambiguity warning).  I haven't thought through the case if Left and
Right both virtually inherit from a common base class, but I think
there would still be two vtables.

It's frightening how well these things work themselves out ;-).

>P.S.  While it *is* true that most C++ implementations *do* indeed issue
>an (unwarranted?) error on my code example (above) I have found that one
>particular PC-based implementation (which I shall not name) apparently
>does not... but that is only because of a separate bug in that implementa-
>tion.  (It doesn't even realize that class-specific delete operators are
>supposed to be inherited so it never even tries to use B::delete.  It
>erroneously uses ::delete instead... and ::delete definitely *is* access-
>able, at all points in the code example.  Thus no errors is issued.)

Are you under a non-disclosure agreement?  Otherwise, please warn
other programmers about compiler bugs.  I did some limited tests with
BC++ and Watcom C++ and they appear to allow your code and inherit
`operator delete()' correctly.

Jamshid Afshar
jamshid@ses.com