Topic: Why are virtual destructors required for delete operator?


Author: crussell@netcom.com (Chris Russell)
Date: 31 Mar 93 17:44:41 GMT
Raw View
Given two classes, foo and bar such that bar is derrived from foo, if I
define a delete operator for foo with parameters (void*,size_t), the size
parameter will be invalid invalid under the following circumstance:
   foo *x = new bar;
   delete x;
This limitation is documented in the ARM on page 283.  (This is leading
somewhere.)

Question #1: Why does this limitation exist?  A delete can only be
safely invoked on an object created by new, therefore the correct size
of the object *IS* known.  (If nowhere else, it MUST be stored in some
form with the heap where the memory dynamically allocated... otherwise,
the builtin delete/free would be unable to reclaim the space.)  Indeed,
this fact can be proven by the fact that the above code snippit WORKS
if the default delete operator is invoked.

    o The default delete operator works correctly because it does not
      need to know the size of the memory to deallocate.  Instead, it
      simply passes the memory pointer to free, which knows the correct
      size of the memory on the heap.  It would be a simple matter to
      add the capability to query the heap on the correct size of a
      memory object.  [Note: other implementations are possible, but
      inevitably someone knows the correct size of the object.]

The supported away to correct this problem is to declare a virtual
desctuctor.  It must be virtual, otherwise it won't work properly.  If
a virtual destructor is declared, then the delete operator will be
given the correct size.  Additionally, it both foo and bar have delete
operators, the correct one will not be called for the code snippit
unless a virtual destructor is defined.  (On CFRONT 3.0.1, the virtual
destructor need only be defined for the base class, but the correct size
information and delete operator resolution will filter down to all derrived
classes.  I don't know if this is correct, ie: in the ARM, behavior yet.
The desctuctor need not contain any code... virtual ~foo(){} is sufficient.)

Question #2: Why is a virtual desctructor tied so closely to the
delete operator, such that is even dictates the virtual resolution of
which delete operator to call.  This is surely obscure and
undesirable.  If it is necessary to determine whether the delete
operator requires a virtual call resolution (and hence a pointer to a
virtual call table), then it seems more obvious to specify "virtual
operator delete."  (Even this is unappealing, but I feel it is a
better solution.)  I feel that requiring a virtual destructor to
ensure proper behavior of other methods is pure hackery in the C++
language specification and should be avoided.  It obscures proper behavior.


Thank you in advance for any responses, corrections, or rational to the
language specification.


Chris Russell            Custom software, networks, CASE tools, and consulting
Adaptive Solutions       Sun SPARC, SGI IRIS, HP Apollo, Macintosh, & PC

Internet:  crussell@netcom.com
GTE:       909/861-4048
U.S. Mail: P.O. Box 5424
           Diamond Bar, CA 91675-7424




Author: jamshid@emx.cc.utexas.edu (Jamshid Afshar)
Date: 1 Apr 93 03:12:59 GMT
Raw View
Btw, I noticed you posted this separately to both comp.lang.c++ and
comp.std.c++.  It's better to crosspost USENET articles instead of
posting the same article separately, and probably better still to
redirect followups to only one of the groups.

In article <crussellC4rLAI.792@netcom.com> crussell@netcom.com (Chris Russell) writes:
>Question #1: Why does this limitation exist?  A delete can only be
>safely invoked on an object created by new, therefore the correct size
>of the object *IS* known [at least by heap]

It's not necessarily true that the actual size of the object is known
by the heap.  A heap may round the requested size up to an integral
number of words or it may allocate fixed-sized blocks (eg, I think
BC++'s default allocater allocates only in 16-byte chunks).

>Indeed, this fact can be proven by the fact that the above code
>snippit WORKS if the default delete operator is invoked.

Remember, nothing in C or C++ is "proven" correct because it "works"
(or appears to work).

>    o [...] It would be a simple matter to add the capability to query
>      the heap on the correct size of a memory object. [...]

It would not be a simple addition to the language.  All user-defined
'operator new()' and 'operator delete()' functions would also have to
add your query function.  Furthermore, requiring the heap managers to
store the exact amount of memory that was requested would add
uneccessary, possibly significant, overhead.

>The supported away to correct this problem is to declare a virtual
>desctuctor.  It must be virtual, otherwise it won't work properly.

This seems like the most natural and efficient solution (while still
retaining C compatibility).

>[...] (On CFRONT 3.0.1, the virtual
>destructor need only be defined for the base class, but the correct size
>information and delete operator resolution will filter down to all derrived
>classes.  I don't know if this is correct, ie: in the ARM, behavior yet.

This is correct and required behavior.  As with any other virtual
function, derived classes do not need to use the keyword 'virtual'
when declaring the destructor if it was declared virtual in a base
class (once virtual, always virtual).

>Question #2: Why is a virtual desctructor tied so closely to the
>delete operator, such that is even dictates the virtual resolution of
>which delete operator to call.  This is surely obscure and
>undesirable.  If it is necessary to determine whether the delete
>operator requires a virtual call resolution (and hence a pointer to a
>virtual call table), then it seems more obvious to specify "virtual
>operator delete."  (Even this is unappealing, but I feel it is a
>better solution.)  I feel that requiring a virtual destructor to
>ensure proper behavior of other methods is pure hackery in the C++
>language specification and should be avoided.  It obscures proper behavior.

Look at it this way: if you delete an object through a pointer to a
base class, the destructor *must* be virtual (period).  I view the
calling of the correct 'T::operator delete()' as being a side-effect
of, and not any more important than, calling the correct destructor.

You seem to be confusing the 'delete' operator with the 'operator
delete()' function.  The 'delete' operator destroys the object pointed
to and makes sure the function 'operator delete()' gets called.
Therefore, I don't think it's illogical that the destructor must be
virtual in order for the 'delete' operator to work correctly.

What you're proposing would apparently require that both 'T::operator
delete()' and the destructor be declared virtual, or that
'T::operator delete()' must always be defined and declared virtual
when you want a virtual destructor.  This seems more cumbersome and
error-prone than the current solution.  Also remember that
'T::operator delete()' and 'T::operator new()' are *static* member
functions (even if not explicitly declared 'static'), and 'virtual
static' is not supported.

The ARM commentary probably explains why destructors are not
'virtual' by default (C compatibility, overhead).  I don't think
it's a burdon to declare destructors virtual and this situation
shouldn't be error-prone if compilers give a warning when a
non-virtual destructor should probably be made virtual.

To hopefully help you understand the reasoning better, I'll present
what I think may be a common implementation technique.  Virtual
destructors are given a hidden extra parameter which indicates whether
the destructor should call 'operator delete(this)' after calling the
destructors for its subobjects.  Normally a compiler could call
'operator delete(p)' for the code 'delete p;', but if the object
pointed to has a virtual destructor, the compiler must leave the call
to 'operator delete()' up to the destructor because the pointer may
not be the same value returned by 'operator new()' (e.g. imagine a
pointer to a virtual base class).

        C++ code                       Possibly what compiler produces
        ------------------------------ ----------------------------------
        Base::~Base() {}               Base::~Base(int __del) {
        // declared virtual               if (__del)
                                             operator delete(this);
                                       }

        Derived::~Derived() {}         Derived::~Derived(int __del) {
                                          this->Base::~Base(0);
                                          if (__del)
                                             operator delete(this);
                                       }

        void kill(String* s) {         void kill(String* s) {
           delete s;                      s->String::~String();
           //String dtr not virtual       operator delete(s);
        }                              }

        void kill(Base* b) {           void kill(Base* b) {
           delete b;                      b->~Base(1);  // does virtual call
        }                              }

Did I make any sense? (I'm really curious -- anyone please comment).

Jamshid Afshar
jamshid@emx.utexas.edu