Topic: Exceptions and destructors


Author: James Kanze <james-albert.kanze@vx.cit.alcatel.fr>
Date: 1997/04/17
Raw View
"Paul D. DeRocco" <strip_these_words_pderocco@ix.netcom.com> writes:

|>  James Kanze wrote:
|>  >
|>  >         struct B : A { B() ; ~B() ; } ;
|>  >
|>  >         struct C : B { C() ; ~C() ; } ;
|>  >
|>  >         {
|>  >                 C*  p = new C ;
|>  >                 //  ...
|>  >                 delete p ;
|>  >         }
|>  >
|>  > With regards to the case where B::~B() throws in the delete, it is
|>  > interesting to look at the exact wording in the standard: "If the object
|>  > or array was allocated in a new-expression and the new-expression does
|>  > not contain a new-placement, the deallocation function is called to free
|>  > the storage occupied by the object; ..."  Note that it does not say "If
|>  > the exception occurs in a new expression.." or anything similar.  It
|>  > says "if the object or array was allocated in a new-expression...".
|>  > Now, the object being destructed in the expression "delete p", above,
|>  > was most definitly allocated in a new expression, so I would argue that
|>  > the deallocation function should be called.
|>
|>  I think you're misreading the standard. (I'm assuming you're reading
|>  [except.ctor], right?) It's talking about what should happen if C::C
|>  throws an exception, not C::~C. If the constructor fails, then the
|>  memory is deallocated, except in the case where it wasn't really
|>  allocated, which is the case when placement-new is used.

This is one interpretation.  Although the label says [except.ctor], the
section heading is "Constructors and destructors".  The first sentence
of paragraph 2 (which is really what I am trying to interpret) refers to
"partially constructed objects"; this term is never defined in the
standard, but one (IMHO probably the more reasonable) interpretation is
that a destructor "deconstructs" objects, so that after entering the
destructor of C, you have a partially constructed object again.

If this interpretation is not the correct one, then the draft is
sigularly silent concerning what happens with exceptions thrown by a
sub-object.  If the standard says nothing in this regard, I believe that
it is to be considered undefined behavior.  Which would pretty much mean
yet another case where you cannot let an exception escape from a
destructor.  (FWIW: The case is difficult to detect from within the
destructor, and the results are a lot worse than calling terminate.)

--
James Kanze      home:     kanze@gabi-soft.fr        +33 (0)1 39 55 85 62
                 office:   kanze@vx.cit.alcatel.fr   +33 (0)1 69 63 14 54
GABI Software, Sarl., 22 rue Jacques-Lemercier, F-78000 Versailles France
     -- Conseils en informatique industrielle --
---
[ comp.std.c++ is moderated.  To submit articles: Try just posting with your
                newsreader.  If that fails, use mailto:std-c++@ncar.ucar.edu
  comp.std.c++ FAQ: http://reality.sgi.com/austern/std-c++/faq.html
  Moderation policy: http://reality.sgi.com/austern/std-c++/policy.html
  Comments? mailto:std-c++-request@ncar.ucar.edu
]





Author: "Paul D. DeRocco" <strip_these_words_pderocco@ix.netcom.com>
Date: 1997/04/18
Raw View
James Kanze wrote:

> This is one interpretation.  Although the label says [except.ctor], the
> section heading is "Constructors and destructors".  The first sentence
> of paragraph 2 (which is really what I am trying to interpret) refers to
> "partially constructed objects"; this term is never defined in the
> standard, but one (IMHO probably the more reasonable) interpretation is
> that a destructor "deconstructs" objects, so that after entering the
> destructor of C, you have a partially constructed object again.

As you pointed out, it would be very difficult to make an implementation
that interpreted the rule this way, since you'd have to remember how
new'ed objects were created (and probably a few other gotchas). I've
always read that clause as referring only to what goes on during
construction, not destruction, since that reading is pretty
straightforward to implement.

> If this interpretation is not the correct one, then the draft is
> sigularly silent concerning what happens with exceptions thrown by a
> sub-object.  If the standard says nothing in this regard, I believe that
> it is to be considered undefined behavior.  Which would pretty much mean
> yet another case where you cannot let an exception escape from a
> destructor.  (FWIW: The case is difficult to detect from within the
> destructor, and the results are a lot worse than calling terminate.)

I'm one of those people who believes that exceptions should be illegal
during destructors, punishable by death (of the program, that is). I'd
like to see any exception that leaks out of a destructor call terminate,
even if it's not during the stack unwinding resulting from some other
exception. I think that's the only rule that doesn't open a rather large
can of worms.

--

Ciao,
Paul D. DeRocco

(Please send e-mail to mail:pderocco@ix.netcom.com instead of the
return address, which has been altered to foil junk mail senders.)
---
[ comp.std.c++ is moderated.  To submit articles: Try just posting with your
                newsreader.  If that fails, use mailto:std-c++@ncar.ucar.edu
  comp.std.c++ FAQ: http://reality.sgi.com/austern/std-c++/faq.html
  Moderation policy: http://reality.sgi.com/austern/std-c++/policy.html
  Comments? mailto:std-c++-request@ncar.ucar.edu
]





Author: James Kanze <james-albert.kanze@vx.cit.alcatel.fr>
Date: 1997/04/15
Raw View

In comp.lang.c++.moderated recently, there has been a discussion of
exception safety, and in particular, of what state an object is left in
when the destructor of a sub-object throws an exception.  I'd like to
present my interpretation of what I think the draft standard says here,
and ask that any members of the committee who were involved with the
specification of exceptions criticize it.  (This is a tentative
interpretation, and I'm not at all sure of it.  Also, it's a bit long
because I am feeling my way around.)

(In the following, I will speak of the "standard".  Whenever I use the
word standard, it is in fact the current committee draft that I am
referring to.)

First, I'm basing my interpretation mainly on paragraph 2 of section
15.2, which starts out: "An object that is partially constructed will
have destructors executed only for its fully constructed sub-objects."

The first point I see as important is that this concerns all composite
objects (all object which have sub-objects).  Thus, given the following:

 struct A { A() ; ~A() ; } ;
 struct B : A { B() ; ~B() ; } ;

 struct C : B { C() ; ~C() ; } ;
 struct D { A a1 ; A a2 ; A a3 ; } ;
 typedef A E[ 3 ] ;

The cases of C, D and E are exactly the same, and an exception thrown in
the constructor/destructor of the B sub-object of C, D::a2 or E[1] will
have exactly the same effect.

The second point is that as far as I can tell, the standard never
actually defines "partially constructed", so we have to base our
interpretation on standard English usage.  I see in fact two possible
interpretations: 1) an object is "partially constructed" between the
moment the first constructor of a sub-object starts execution, and the
moment that the constructor of the complete object finishes, or 2) in
addition to 1, an object is also "partially constructed" between the
moment the destructor of the comlete object is entered and the moment
the last destructor of a sub-object finishes.  Although a superficial
reading of the expression might suggest 1, I think that given the
overall implications of the standard (e.g.: a destructor "deconstructs"
the object), only 2 really fits in.  In all other contexts (e.g.: RTTI,
virtual function call resolution), the destructors mirror exactly the
constructors.  Following this line of reasoning, I would also suppose
that the sub-object ceases to be "fully constructed" as soon as its
destructor is entered.

Up until this point, I don't really think that there is much room for
discussion.  Although the standard could be clearer, particularly with
regards to "partially constructed" and destructors, I think it is
adequate.  Thus, for example, given the above declarations, and the
following:

 {
  C   c ;
  //  ...
 }

If the constructor B:B() of object c exits via an exception, the
destructor A::~A(), and no other destructors, will be called.  This is
explicit in the draft.  Similarly, if the destructor B::~B() of the
object c exits via an exception (when the object goes out of scope),
then A::~A() (and no others) will be called as part of the process of
stack unwinding.  This is from the definition 2 of "partially
constructed", above.  (Note that if A::~A() exits via an exception in
either case above, terminate will be called.)

Now consider a slight variant of the above:

 {
  C*  p = new C ;
  //  ...
  delete p ;
 }

Here, we are concerned not only with the destructors which might be
called, but with what happens to the allocated memory.  Again, the case
if the exception is thrown in the new is perfectly clear, as it is
treated explicitly in the standard.

With regards to the case where B::~B() throws in the delete, it is
interesting to look at the exact wording in the standard: "If the object
or array was allocated in a new-expression and the new-expression does
not contain a new-placement, the deallocation function is called to free
the storage occupied by the object; ..."  Note that it does not say "If
the exception occurs in a new expression.." or anything similar.  It
says "if the object or array was allocated in a new-expression...".
Now, the object being destructed in the expression "delete p", above,
was most definitly allocated in a new expression, so I would argue that
the deallocation function should be called.

This would seem clear enough, except for two things:

1. The only thing preventing it from applying to all exceptions which
occur within an object is the fact that it occurs in a paragraph that is
talking about "partially constructed objects."  So we must come back to
the definition of "partially constructed", above.

2. The implementation is tricky, to say the least, since not only can
the new and the delete be in different modules, different control flows
may cause the same delete to be executed for different new expressions.
Basically, the implementation must somehow "memorize" the delete that is
to be called.  This is fundamentally more difficult than memorizing the
number of destructors to call in delete[], in that the implementation
must not only memorize which operator delete to call, but must also
memorize any additional arguments which might be significant.  It also
means that at compile time, the compiler doesn't know how many arguments
it will actually have to pass to delete.  These problems are solvable,
but at significant cost to users who don't use placement new.

Thus, I'm not at all sure that this is the solution that the committee
actually intended.  On the other hand, however, if it isn't, it is very
difficult to see how the user can handle the problem if the memory is
not automatically deleted.  The user cannot call delete on the pointer
value again, since part of the object has already been destructed.  (It
may be possible if the object has a virtual destructor, but this only
covers the case of inhertance, not of membership or array elements.)  On
the other hand, the user cannot call operator delete directly to free
the memory, since 1) that would leave some sub-objects undestructed, and
2) in the case of arrays, he doesn't even know the address returned by
operator new.

(The more I think about it, the more I'm convinced that the easiest
solution for the committee, and a perfectly acceptable one for the user,
is to declare that all destructors have an implicit "throw ()" exception
specification.  See Scott Meyers for the reasons why the user doesn't
want to exit a destructor via an exception.)

--
James Kanze      home:     kanze@gabi-soft.fr        +33 (0)1 39 55 85 62
                 office:   kanze@vx.cit.alcatel.fr   +33 (0)1 69 63 14 54
GABI Software, Sarl., 22 rue Jacques-Lemercier, F-78000 Versailles France
     -- Conseils en informatique industrielle --
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: fjh@murlibobo.cs.mu.OZ.AU (Fergus Henderson)
Date: 1997/04/15
Raw View

James Kanze <james-albert.kanze@vx.cit.alcatel.fr> writes:

> struct A { A() ; ~A() ; } ;
> struct B : A { B() ; ~B() ; } ;
>
> struct C : B { C() ; ~C() ; } ;
...
> {
>  C*  p = new C ;
>  //  ...
>  delete p ;
> }
...
>With regards to the case where B::~B() throws in the delete, it is
>interesting to look at the exact wording in the standard:

The wording in the current draft just doesn't cover this case.
You can try to interpret the current wording as saying something
sensible about this case, but the extent to which it does or
does not is purely accidental.

Note that 5.3.5[expr.delete]/7 says "To free the storage pointed to, the
delete-expression will call a deallocation function."  No ifs or buts,
and no talk about exceptions there, so one reasonable interpretation is
that it must always call a deallocation function, whether or not the
destructor throws an exception.  But another reasonable interpretation
is that the standard does not say what happens if an exception is
thrown, therefore if an exception is thrown, the behaviour is
undefined.

The current draft is definitely inadequate when it comes to specifying
what happens when destructors throw exceptions.  It ought to be fixed.

>2. The implementation is tricky, to say the least, since not only can
>the new and the delete be in different modules, different control flows
>may cause the same delete to be executed for different new expressions.
>Basically, the implementation must somehow "memorize" the delete that is
>to be called.

That is definitely not the intent of the committee.
If the destructor throws, delete should call the same deallocation
function that it would have if the destructor had returned.

--
Fergus Henderson <fjh@cs.mu.oz.au>   |  "I have always known that the pursuit
WWW: <http://www.cs.mu.oz.au/~fjh>   |  of excellence is a lethal habit"
PGP: finger fjh@128.250.37.3         |     -- the last words of T. S. Garp.
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: "Paul D. DeRocco" <strip_these_words_pderocco@ix.netcom.com>
Date: 1997/04/16
Raw View

James Kanze wrote:
>
>         struct B : A { B() ; ~B() ; } ;
>
>         struct C : B { C() ; ~C() ; } ;
>
>         {
>                 C*  p = new C ;
>                 //  ...
>                 delete p ;
>         }
>
> With regards to the case where B::~B() throws in the delete, it is
> interesting to look at the exact wording in the standard: "If the object
> or array was allocated in a new-expression and the new-expression does
> not contain a new-placement, the deallocation function is called to free
> the storage occupied by the object; ..."  Note that it does not say "If
> the exception occurs in a new expression.." or anything similar.  It
> says "if the object or array was allocated in a new-expression...".
> Now, the object being destructed in the expression "delete p", above,
> was most definitly allocated in a new expression, so I would argue that
> the deallocation function should be called.

I think you're misreading the standard. (I'm assuming you're reading
[except.ctor], right?) It's talking about what should happen if C::C
throws an exception, not C::~C. If the constructor fails, then the
memory is deallocated, except in the case where it wasn't really
allocated, which is the case when placement-new is used.

--

Ciao,
Paul D. DeRocco

(Please send e-mail to mail:pderocco@ix.netcom.com instead of the
return address, which has been altered to foil junk mail senders.)
---
[ comp.std.c++ is moderated.  To submit articles: try just posting with      ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]