Topic: Exceptions from destructors (Was broken std::uncaught_exception() (Was: Exception Handling))


Author: "Martijn Lievaart" <nobody@orion.nl>
Date: 1998/11/20
Raw View
AllanW@my-dejanews.com wrote in message <72vdba$omd$1@nnrp1.dejanews.com>...
>In article <72rsuq$mb4@news3.euro.net>,
>  "Martijn Lievaart" <nobody@orion.nl> wrote:
>>
>> So maybe we should code it like this?
>>
>>     File file( "foo" ); // buffers writes in its cache
>>     file.write( "blabla" ); // now something is in the buffer
>>     func_that_could_throw(); // throws exception
>>
>> File::~File() // called duing stack unwinding
>> {
>>     if (std::uncought_exception())
>>         try {
>>             close();
>>         } catch (...) {}    // just ignore
>>     else
>>         close();    // do report errors
>>     // ...
>> }
>>
>> This is unwieldy to say the least, but it solves the problem when we just
>> want to ignore subsequent errors.
>
>Others have already suggested:
>
>    File file( "foo" ); // buffers writes in its cache
>    file.write( "blabla" ); // now something is in the buffer
>    func_that_could_throw(); // throws exception
>    file.close();
>
>    File::~File() // called duing stack unwinding
>    {
>        try {
>            close();
>            // ...
>        } catch (...) {}    // just ignore
>    }
>

Hmmm, I'm not in favor of this. I've to often found that some seemingly
"impossible to fail" operation did indeed fail. This construct invites
programmers to *not* close the file (hey, the destructor does that for me!).
Therefore it can hide errors that otherwise would have been noticed. These
are unlikely errors, but those are the most difficult to track down.

But if others have suggested this, maybe someone has some pointers for me to
follow?

Martijn
--
Microsoft logic. Add signature under dash-dash-space.
Then strip all trailing spaces (Outlook Express 4.71)
My email address is intentionally invalid against spam
reply to mlievaart at orion in nl
---
[ 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    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: AllanW@my-dejanews.com
Date: 1998/11/18
Raw View
In article <72rsuq$mb4@news3.euro.net>,
  "Martijn Lievaart" <nobody@orion.nl> wrote:

> Hello,

Hi, Martijn!

> Gargantua Blargg wrote in message
> <199811130800.CAA16992@centurion.flash.net>...
> >Just looking at throwing an exception from a destructor, the major problem
> >of the object not being destroyed comes up:
> >
> >    class X {
> >        auto_ptr<int> use_count;
> >        X() { /* ... */ }
> >        ~X()
> >        {
> >            if ( bla bla )
> >                throw XYZ();
> >            // is use_count destructed even when the throw occurs?
> >        }
> >    };
>
> Well I would certainly expect use_count to be destructed! Can anybody verify
> this?

    15.2 Constructors and destructors [except.ctor]

  2 An object that is partially constructed or partially destroyed
    will have destructors executed for all of its fully constructed
    subobjects, that is, for subobjects for which the constructor
    has completed execution and the destructor has not yet begun
    execution. Should a constructor for an element of an automatic
    array throw an exception, only the constructed elements of that
    array will be destroyed. ...

However, I tried this in MSVC5 and it failed; control never reached
the destructors for base or contained items. In general I think it
may be some time before you can assume that most platforms get this
right.

> >I think that in general, classes need to be written in every way possible
> >to insure that destruction will not require any resources, or any
> >operations that could fail. Obviously some classes will be written
> >specifically to throw an exception in this case, but the majority won't.
> >Those that intentionally throw an exception (an AutoThrowErrorCode that
> >throws an exception if the error code is not extracted, for example)
> >should be treated like a throw (by the user), and shouldn't be created
> >where an exception can be thrown during their lifetime, so they aren't a
> >problem.
> >
>
> However, if this situation *does* occur, it is a program logic error.
> uncought_exception can help here to assert(). This is a situation where I
> find uncought_exception to be a big win.
>
> >This leaves those "normal" classes that have possible errors in
> >destruction (writing to a file, for example). As others have suggested, I
> >usually add explicit close() (and similar) functions that should be called
> >by the user just before the object is destructed(), to prevent errors from
> >happening in the destructor. But even this isn't enough, since an
> >exception could occur between the use of this object and the call to
> >close():
> >
> >    File file( "foo" ); // buffers writes in its cache
> >
> >    file.write( "blabla" ); // now something is in the buffer
> >
> >    func_that_could_throw(); // throws exception
> >
> >    file.flush(); // flushes buffer and checks for write errors
> >
> >File::~File() // called duing stack unwinding
> >{
> >    flush(); // gotta do this before closing
> >    close();
> >    // ...
> >}
> >
> >Since the flush in the user code is bypassed by the exception, a write
> >error could now occur in File's destructor. So is the user supposed to
> >call error-prone transaction completion functions for objects after every
> >use, before any point an exception could be thrown? Maybe so.
> >
>
> No, this is a good idea in general, but not a panacea. F.i. close can throw
> as well! (Or can you *prove* it doesn't? In that case I can come up with
> another example, you just cannot always prove that things will not throw)
>
> So maybe we should code it like this?
>
>     File file( "foo" ); // buffers writes in its cache
>     file.write( "blabla" ); // now something is in the buffer
>     func_that_could_throw(); // throws exception
>
> File::~File() // called duing stack unwinding
> {
>     if (std::uncought_exception())
>         try {
>             close();
>         } catch (...) {}    // just ignore
>     else
>         close();    // do report errors
>     // ...
> }
>
> This is unwieldy to say the least, but it solves the problem when we just
> want to ignore subsequent errors.

Others have already suggested:

    File file( "foo" ); // buffers writes in its cache
    file.write( "blabla" ); // now something is in the buffer
    func_that_could_throw(); // throws exception
    file.close();

    File::~File() // called duing stack unwinding
    {
        try {
            close();
            // ...
        } catch (...) {}    // just ignore
    }

--
AllanW@my-dejanews.com is a "Spam Magnet" -- never read.
Please reply in USENET only, sorry.

-----------== Posted via Deja News, The Discussion Network ==----------
http://www.dejanews.com/       Search, Read, Discuss, or Start Your Own
---
[ 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    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: Valentin Bonnard <bonnardv@pratique.fr>
Date: 1998/11/20
Raw View
Martijn Lievaart <nobody@orion.nl> writes:

> It should be noted that it mainly boils down to the question how to
> handle multiple errors.

Exactly.

Note that this is in the problem domain, so the usual "don't do
anything in a dtor", "that's bad style", "you should have checked
that before (if you care about it)" ranting won't do anything about
it.

This discution already occured here, and I have already proposed
a solution to this problem: a double_exception_handler (or give it
whatever name).

When a dtor exits with a exception, and it has been invoked
because of another exception, call the double_exception_handler.

This function musn't return. It can abort with an error message
if the programmer wants, or it can throw an exception which
summarize both exceptions. Note that, by calling terminate,
behaviour compatible with what we currently have could be achieved.

typedef NoConstruct (*double_exception_handler) (Exception, Exception);
void set_double_exception_handler (double_exception_handler_t);
double_exception_handler_t get_double_exception_handler ();

[NoConstruct has no instances because it's impossible to construct
them; remember that the handler must not return a value.]

[How to define the type Exception is a non-trivial, but
solvable side issue.]

For any pair of exception, we could throw std::pair<e1, e2>.

Or we could throw the maximum, ie the worse exception.

logic_errors are worse than runtime_error because they
often indicate (not always) a design/coding flaw (bad_alloc
should be considered a runtime_error, bad_cast and bad_typeid
logic_errors for this purpose). bad_alloc is probably more
important than any other runtime error, because it affects
memory, which is necessary to think.

For user defined exception, it's up to the user to decide
which exception is more important.

--

Valentin Bonnard                mailto:bonnardv@pratique.fr
info about C++/a propos du C++: http://pages.pratique.fr/~bonnardv/



[ 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    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: "Martijn Lievaart" <nobody@orion.nl>
Date: 1998/11/17
Raw View
Hello,

I took the liberty of doing some major snippage here. Also I mainly respond
to points being raised without trying to make a logical story out of it all
though there are certain common themes here.

Gargantua Blargg wrote in message
<199811130800.CAA16992@centurion.flash.net>...
>It's late and I need some sleep. Hope this gets the ball rolling in
>someone's head. Thanks for reading.

Well you certainly got me thinking.... You write a convincing story for
someone needing some sleep ;^>

>I think this discussion for a solution is solving the immediate problem at
>hand, namely allowing user code to keep terminate() from being called, yet
>throw exceptions from destructors, but the discussion is not looking at
>the bigger picture, which is throwing exceptions from destructors, and
>problems with that.
>

I think this is a major point, and this is why all proposed solutions would
fail. It should be noted that it mainly boils down to the question how to
handle multiple errors. This question is insolvable without an overall
strategy, which imho is impossible in C++ today because of the wide variety
how errors are reported. A way to handle this would be to have every
exception derive from a common ancester. This ancester would have
functionality to handle multiple errors (f.i. put them on a list or a tree).
This is just a rough idea, so please don't tell me (only) about the problems
it would bring, it is ment to be planted as a seed.

The problem is compounded by the fact that it is often inpossible to predict
what errors can be thrown in advance by any piece of the system. Just think
about network errors, resource quota or the async problems Gargantua tells
about further on in his post.

>
>Just looking at throwing an exception from a destructor, the major problem
>of the object not being destroyed comes up:
>
>    class X {
>        auto_ptr<int> use_count;
>        X() { /* ... */ }
>        ~X()
>        {
>            if ( bla bla )
>                throw XYZ();
>            // is use_count destructed even when the throw occurs?
>        }
>    };
>

Well I would certainly expect use_count to be destructed! Can anybody verify
this?

>
>I think that in general, classes need to be written in every way possible
>to insure that destruction will not require any resources, or any
>operations that could fail. Obviously some classes will be written
>specifically to throw an exception in this case, but the majority won't.
>Those that intentionally throw an exception (an AutoThrowErrorCode that
>throws an exception if the error code is not extracted, for example)
>should be treated like a throw (by the user), and shouldn't be created
>where an exception can be thrown during their lifetime, so they aren't a
>problem.
>

However, if this situation *does* occur, it is a program logic error.
uncought_exception can help here to assert(). This is a situation where I
find uncought_exception to be a big win.

>This leaves those "normal" classes that have possible errors in
>destruction (writing to a file, for example). As others have suggested, I
>usually add explicit close() (and similar) functions that should be called
>by the user just before the object is destructed(), to prevent errors from
>happening in the destructor. But even this isn't enough, since an
>exception could occur between the use of this object and the call to
>close():
>
>    File file( "foo" ); // buffers writes in its cache
>
>    file.write( "blabla" ); // now something is in the buffer
>
>    func_that_could_throw(); // throws exception
>
>    file.flush(); // flushes buffer and checks for write errors
>
>File::~File() // called duing stack unwinding
>{
>    flush(); // gotta do this before closing
>    close();
>    // ...
>}
>
>Since the flush in the user code is bypassed by the exception, a write
>error could now occur in File's destructor. So is the user supposed to
>call error-prone transaction completion functions for objects after every
>use, before any point an exception could be thrown? Maybe so.
>

No, this is a good idea in general, but not a panacea. F.i. close can throw
as well! (Or can you *prove* it doesn't? In that case I can come up with
another example, you just cannot always prove that things will not throw)

So maybe we should code it like this?

    File file( "foo" ); // buffers writes in its cache
    file.write( "blabla" ); // now something is in the buffer
    func_that_could_throw(); // throws exception

File::~File() // called duing stack unwinding
{
    if (std::uncought_exception())
        try {
            close();
        } catch (...) {}    // just ignore
    else
        close();    // do report errors
    // ...
}

This is unwieldy to say the least, but it solves the problem when we just
want to ignore subsequent errors.

>Maybe it would be good to separate the errors into synchronous errors that
>involve the current control flow, and errors that involve another (not
>necessarily real with regard to the OS) thread? However, there is
>certainly code that wants to treat all these erros as synchronous, where
>the order of things matters, and completing one after the other,
>successfully, matters. In these, buffering would probably not be used,
>since it is obviously not possible (with simple buffering that assumes a
>write error will not occur) to buffer yet tell when a write error is going
>to occur before the write is even performed!
>

This is a very interesting notion. It not new though, f.i. in databases.
Everything you commit is either completely in the database, or it is not
(and then the commit fails). This way you can write systems that will behave
predictable and recoverable. Maybe this concept needs widening to any
resource (Transaction servers are a good step in this direction). It fails
however to address the notion of *reporting* multiple errors, it just gives
the tools to write robust programs and deals with the problems adressed here
on a higher level.

In other words, it would give you checkpoints where:
- all asynchronous operations must be completed to a point where any further
error raised by the asynchronous operation will not compromise integrity and
- checkpoints can be placed at places where we are prepared to deal with
errors.

So how does this help with double exceptions? Well honestly, it doesn't
really solve everything. It just gives a framework where we can start
looking for real solutions. Without such a framework and more thought about
how to report on multiple exceptions, imho it is impossible to find a real
solution to this problem.

>Perhaps there are two categories of functions: those that must know
>exactly what external effects have been applied when, and those that are
>only concerned with completing successfully, or, failing that, returning
>an error of the first cause of failure, and cleaning up everything to how
>it was before.
>

The first seems impossible to achieve, I think, so I would go with the
second.

>I'd say the second class is quite common in everyday application code. An
>example would be saving to a file. If it fails due to out of memory, for
>example, we don't care if any pending writes to the file fail. We just
>want to delete the original file and exit the function. In general, this
>kind of semi-rollback is fairly easily implementable.
>

Uhuh, but I want it both automatic (I don't have to write any/much code) and
as a concept so everybody starts thinking about how to make their code safe
in these respects.

>What I'm trying to do is bring this issue from the solving of this
>destructor invocation issue and seeing if we can throw, to a general
>design issue of what works in real systems. At the very least, for me, it
>moves my activity from trying to fix the language, or hating it, to
>solving the problem in the abstract design space, where languages don't
>exist (well, don't have as much influence on thought :-), and then seeing
>how the solution can work with C++ as it currently is defined.
>

It seems that I've moved up to a higher abstract level, and now I'm unsure
how to apply it to real world solutions. I never have been able to come up
with a completely satisfactory solution, but I have found that you *must*
address this in the design fase, otherwise it will come back to hound you
big time. Language solutions help and I think C++ is still lacking in this
respect. Unfortunately it probably takes bigger brains than mine to solve
the problem.

Real world solutions I've encountered/applied are (not neccesary exclusive):
- Ignore the problem
- Be carefull, but don't try to solve every situation
- Look at every situation what the cost of failure is
- Use a database to make things failsafe. This one is very powerfull and can
be used very effectively whenever a database is used in the first place. It
can even be a reason to start using one (have a look at PSE Pro from ODI, a
small single process oodbms that is very cheap).

>Anyway, in the common case, it seems that the we simple want to ignore any
>other errors! Perhaps other errors could be reported during debugging, in
>case there was a programmer error (example: couldn't delete temp file
>because it was open).
>


The above solution (testing uncought_exception in destructors) does just
this, though in a admittedly tedious, ugly and error-prone way. Maybe
something like this could be introduced to the language, but then it should
be clear it is a only a tool, only to be used when we have no other option
but to ignore any exception a destructor throws because a more important
exception is in progress. However, we have this solution today with
uncought_exception and it is not a structural solution, as noted in this
thread. So never mind.

Just my fl 0,02
martijn
--
Microsoft logic. Add signature under dash-dash-space.
Then strip all trailing spaces (Outlook Express 4.71)
My email address is intentionally invalid against spam
reply to mlievaart at orion in nl




[ 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    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]