Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Developing an exception hierarchy

367 views
Skip to first unread message

jens.m...@googlemail.com

unread,
May 9, 2012, 3:25:06 PM5/9/12
to
{ reformatted - please limit your lines to 70 characters }

Hi,

we are trying to develop a company standard hierarchy for exception
handling in our software together with a small set of rules when to
throw which exception. I thought that it would be a good idea to base
our hierarchy on the standard exceptions and re-use the hierarchy from
there, but we are having trouble to understand some of the standard
exceptions and when to use which one. The standard is kind of short
when describing the standard exceptions, it basically described the
differences between runtime_error and logic_error. Derived exceptions
are described very shortly in statements such as "The class
invalid_argument defines the type of objects thrown as exceptions to
report an invalid argument", which is kind of self-referencing. I am
going to list some questions and interpretations in the following in
the hope that some discussion could clarify things.

1. std::overflow_error and std::underflow_error
These two are derived from std::runtime_error which is the base class
for "events beyond the scope of the program". IMO, overflow and
underflow are more logic errors in the program and not dependent on
the runtime in general.

2. std::domain_error and std::invalid_argument
We came up with a distinction saying that a domain_error is thrown
when a value does not belong to the "static" domain of a function,
e.g. a function that only works in positive
numbers. std::invalid_argument should be thrown when a value cannot be
used because the current state of the program permits that,
e.g. putting a value twice into an object that holds unique
values. This container could hold all values of a given type,
e.g. string, but the domain changes when values are inserted.

3. std::range_error
I have no idea under what conditions a std:range_error should be
thrown and not a domain_error, out_of_range_error, overflow_error,
underflow_error. The only two functions throwing range_error are
wstring_convert::from_bytes and wstring_convert::to_bytes, but I don't
see why these two should not throw another exception.

4. std::out_of_range and std::length_error seem to be a special case of
an invalid argument, so why is this not a subclass of std::invalid_argument?

5. std::regex_error
This class is thrown when a string is not a valid regex, which is a
domain error, but it is derived from std::runtime_error. It is also
thrown when the state machine cannot be constructed due to illegal
characters, which also seems more like a domain error than something
outside the scope of the program.

6. std::bad_weak_ptr and std::bad_function_call
These two class are directly derived from std::exception instead of
std::logic_error. I think that is so because these present severe bugs
in your program and not exceptions.

Greetings from rainy Germany,
Jens


--
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]

Wil Evers

unread,
May 9, 2012, 6:03:50 PM5/9/12
to
jens.m...@googlemail.com wrote:

> we are trying to develop a company standard hierarchy for exception
> handling in our software together with a small set of rules when to
> throw which exception. I thought that it would be a good idea to
> base our hierarchy on the standard exceptions and re-use the
> hierarchy from there, but we are having trouble to understand some
> of the standard exceptions and when to use which one. The standard
> is kind of short when describing the standard exceptions, it
> basically described the differences between runtime_error and
> logic_error. Derived exceptions are described very shortly in
> statements such as "The class invalid_argument defines the type of
> objects thrown as exceptions to report an invalid argument", which
> is kind of self-referencing. I am going to list some questions and
> interpretations in the following in the hope that some discussion
> could clarify things.

Forget it. IMO, the standard exception hierarchy is close to
useless.

First, there is 'logic_error', which is based on the belief that it
makes sense to use exceptions to report errors in the program's logic.
C++ has a much better way of dealing with logic errors: emit a
diagnostic and abort. The second-to-last thing we want on a logic
error is stack unwinding; the only one thing worse is trying to resume
business as usual after catching one.

That leaves the other half of the hierarchy: runtime_error. That half
is, I think, meant for exceptions that couldn't reasonably be
anticipated. The problem with std::runtime_error is that it holds a
std::string to store the error message, which implies you cannot
safely throw one when running out of resources. The official
situation in C++11 seems to have improved somewhat, but in many
implementations, throwing a runtime_error is still equivalent to
risking a call to terminate() because of a double fault.

The only standard exception type left is std::exception. All it can
do is return an implementation-defined pointer to a character array
that will hopefully tell us what went wrong. That's, of course, a
reasonable start, but otherwise not very helpful for catching a more
specific type of exception and reacting appropriately.

In short: derive your exceptions from std::exception and make sure to
provide as sensible override for "const char *what() const throw()".
Other than that, I'm afraid you're on your own.

Regards,

- Wil

Agents Marlow

unread,
May 10, 2012, 2:48:13 PM5/10/12
to
On May 9, 11:03 pm, Wil Evers <boun...@dev.null> wrote:
> jens.muad...@googlemail.com wrote:
> > we are trying to develop a company standard hierarchy for
> > exception handling in our software together with a small set of
> > rules when to throw which exception. I thought that it would be a
> > good idea to base our hierarchy on the standard exceptions

I think that is a good idea. It means that if/when there is ever some
code that forgets to catch your exception it can be caught via a
suitable base class.

> Forget it. IMO, the standard exception hierarchy is close to
> useless.

I beg to differ.

> First, there is 'logic_error', which is based on the belief that it
> makes sense to use exceptions to report errors in the program's
> logic. C++ has a much better way of dealing with logic errors: emit
> a diagnostic and abort.

That is not acceptable in some situations. I worked on a very large
server app where each function offered by the server was relatively
isolated. With your approach a logic error in one server function
would bring down the entire server and severe the connection to all
its clients. So that our function never did this we used an exception
that meant 'an assertion has failed'. It inherited from
std::logic_error. We caught it at the entry to our function, where we
reported it then exited our function. Users would report that their
command failed but the server itself did not fail.

> That leaves the other half of the hierarchy: runtime_error. That
> half is, I think, meant for exceptions that couldn't reasonably be
> anticipated.

Hmm. File not found, database connection lost, insufficient resources,
etc are all examples of runtime errors. These can be expected to occur
but they are not usually part of the 'happy path'. They still need to
be catered for. For any that specifically need to be catered for,
maybe several levels higher up, I would use an application exception
that inherits from std::runtime_error.

> The problem with std::runtime_error is that it holds a std::string
> to store the error message, which implies you cannot safely throw
> one when running out of resources.

Almost. You cannot safely throw one when running out of memory. But
when that happens std::bad_alloc tends to get thrown.

> The only standard exception type left is std::exception. All it can
> do is return an implementation-defined pointer to a character array
> that will hopefully tell us what went wrong. That's, of course, a
> reasonable start, but otherwise not very helpful for catching a more
> specific type of exception and reacting appropriately.

Inheriting from the base class prevents anyone distinguishing one
exception type from another.

> In short: derive your exceptions from std::exception and make sure
> to provide as sensible override for "const char *what() const
> throw()". Other than that, I'm afraid you're on your own.

If you do that and someone above you catches std::runtime_error then
they will not catch your exception.

Regards,

Andrew Marlow

Wil Evers

unread,
May 10, 2012, 5:28:53 PM5/10/12
to
Agents Marlow wrote:

> On May 9, 11:03 pm, Wil Evers <boun...@dev.null> wrote:

>> First, there is 'logic_error', which is based on the belief that it
>> makes sense to use exceptions to report errors in the program's
>> logic. C++ has a much better way of dealing with logic errors:
>> emit a diagnostic and abort.
>
> That is not acceptable in some situations. I worked on a very large
> server app where each function offered by the server was relatively
> isolated. With your approach a logic error in one server function
> would bring down the entire server and severe the connection to all
> its clients. So that our function never did this we used an
> exception that meant 'an assertion has failed'. It inherited from
> std::logic_error. We caught it at the entry to our function, where
> we reported it then exited our function. Users would report that
> their command failed but the server itself did not fail.

That seems very brittle to me. A logic_error reports a problem is the
program's logic; it doesn't say the error's effects are limited to the
state of the current connection.

Unless you control all of the program's code, there is no way to know:
the logic_error could have been thrown from your own code, or -
because it is a standardized exception type - from some third-party
component that is unaware of your interpretation.

The way to be sure is to somehow distinguish between exceptions that
are known to be locally recoverable from the rest; a straightforward
approach would be to use a dedicated exception type, different from a
plain logic_error, for that.

>> That leaves the other half of the hierarchy: runtime_error. That
>> half is, I think, meant for exceptions that couldn't reasonably be
>> anticipated.
>
> Hmm. File not found, database connection lost, insufficient
> resources, etc are all examples of runtime errors. These can be
> expected to occur but they are not usually part of the 'happy
> path'. They still need to be catered for. For any that specifically
> need to be catered for, maybe several levels higher up, I would use
> an application exception that inherits from std::runtime_error.
>
>> The problem with std::runtime_error is that it holds a std::string
>> to store the error message, which implies you cannot safely throw
>> one when running out of resources.
>
> Almost. You cannot safely throw one when running out of memory. But
> when that happens std::bad_alloc tends to get thrown.

I wouldn't bet on that. At least for C++03, both the expert community
and some common C++ implementations disagreed on what is supposed to
happen when the program runs into an exception while initializing the
object carrying the value of a throw-expression. Some thought this
should result in a call to terminate(), while others advocated
propagating the exception that caused the initialization of the
exception object to fail.

Regards,

- Wil

Joshua Maurice

unread,
May 10, 2012, 7:22:15 PM5/10/12
to
On May 10, 11:48 am, Agents Marlow <marlow.age...@gmail.com> wrote:
> On May 9, 11:03 pm, Wil Evers <boun...@dev.null> wrote:
>
> > First, there is 'logic_error', which is based on the belief that it
> > makes sense to use exceptions to report errors in the program's
> > logic. C++ has a much better way of dealing with logic errors: emit
> > a diagnostic and abort.
>
> That is not acceptable in some situations. I worked on a very large
> server app where each function offered by the server was relatively
> isolated. With your approach a logic error in one server function
> would bring down the entire server and severe the connection to all
> its clients. So that our function never did this we used an exception
> that meant 'an assertion has failed'. It inherited from
> std::logic_error. We caught it at the entry to our function, where we
> reported it then exited our function. Users would report that their
> command failed but the server itself did not fail.

This is a holy war of sorts, but I'll throw-in by saying the
following:

If you have checks in your code that "makes sure" that your code obeys
certain invariants, and those checks fail, then something very bad has
happened. The default action in such a situation is to die and dump
core. If your code is broken, you probably want to figure out why, and
if your code is broken, you probably don't want to unwind and cause
destructors to operate on your borked state.

If this contradicts up-time guarantees that your product needs, then
the solution is to introduce fault tolerance. Fault tolerance is the
ability to recover from faults or to continue running in the face of
faults, where "fault" means: unexpected conditions or errors.

The holy war is how to achieve fault tolerance and exactly what
constitutes a "fault".

One side, the wrong side, says you should achieve fault tolerance by
guaranteeing that faults aren't fatal and by trying to minimize faults
in your code. Play nice. Don't dump core. Don't SIGSEGV. Instead of
dumping core, throw an exception, return an error code, log an error,
etc.

The other side, my side, the right side, says that you should
introduce fault isolation and redundancy to achieve fault tolerance.
Fault isolation is the ability for one piece of code to be unaffected
by the faults of another piece of code.

Code will have mistakes, and the question is what to do in the face of
the mistakes. The wrong side says just write perfect code. The right
side says ensure that one piece of broken code won't affect another
piece of code by using guarantees of fault isolation, and if required
have redundancy to maintain up-time requirements.

How do you achieve "real fault isolation"? In C and C++, the smallest
unit that has good guarantees of fault isolation is the process. Ergo,
if you want one job to be unaffected by another job's "logic errors",
aka "asserts", failed sanity checks, SIGSEGV, etc., then your jobs
ought to be in separate processes.

Depending on the requirements, separate processes may still lack the
required fault isolation. Perhaps you need separate computers. Perhaps
you need redundancy as well. Perhaps you need separate backup power
supplies. Perhaps you need backup buildings and facilities. Identify
the single modes of failure and try as best as you can to mitigate
them.

PS: Sometimes it makes sense to me to return an error code for an
unexpected condition. Then again, this is just semantic quibbling.
What is an unexpected condition? Is a sanity check assert checking for
an unexpected condition? It's all cost-benefit analysis. You need to
try and determine what is the cheapest (and moral) plans to achieve
your desired ends. I argue that using fault isolation and redundancy
to achieve fault tolerance is usually the best plan, especially as
compared to this idea of throwing exceptions for where I would use a
debug or release assert.

[/rant]

Seungbeom Kim

unread,
May 11, 2012, 12:17:29 AM5/11/12
to
On 2012-05-10 11:48, Agents Marlow wrote:
> On May 9, 11:03 pm, Wil Evers <boun...@dev.null> wrote:
>> First, there is 'logic_error', which is based on the belief that it
>> makes sense to use exceptions to report errors in the program's
>> logic. C++ has a much better way of dealing with logic errors:
>> emit a diagnostic and abort.
>
> That is not acceptable in some situations. I worked on a very large
> server app where each function offered by the server was relatively
> isolated. With your approach a logic error in one server function
> would bring down the entire server and severe the connection to all
> its clients. So that our function never did this we used an
> exception that meant 'an assertion has failed'. It inherited from
> std::logic_error. We caught it at the entry to our function, where
> we reported it then exited our function. Users would report that
> their command failed but the server itself did not fail.

If keeping the server running is an important goal, you cannot really
rely on the assumption that all logic errors, a.k.a. bugs, are
properly detected and handled by C++ exceptions, can you? For example,
a simple segmentation fault will crash the entire program no matter
whether you use exceptions or assertions for other logic errors; a
subtler bug in serving a client may have corrupted data related to
other clients before an exception is thrown and the function is
terminated; etc.

Therefore, you need to build a structure based on a lower-level
assumption; for example, assuming on the OS level that a process will
not be affected by bad behaviour of another process, you can have a
parent process run a loop and fork child processes, which should not
be affected by others. I have worked in a project where a program
forks for a certain sub-task, not because it needs parallelism, but
just because sometimes it may crash from using up the stack.

--
Seungbeom Kim

Seungbeom Kim

unread,
May 11, 2012, 12:32:59 AM5/11/12
to
On 2012-05-09 12:25, jens.m...@googlemail.com wrote:
> Derived exceptions are described very shortly in statements such as
> "The class invalid_argument defines the type of objects thrown as
> exceptions to report an invalid argument", which is kind of
> self-referencing. I am going to list some questions and
> interpretations in the following in the hope that some discussion
> could clarify things.
>
> 1. std::overflow_error and std::underflow_error
> These two are derived from std::runtime_error which is the base
> class for "events beyond the scope of the program". IMO, overflow
> and underflow are more logic errors in the program and not dependent
> on the runtime in general.

I think it's more natural to regard overflow and underflow as runtime
errors in general (especially in floating-point arithmetic); you try a
certain operation and then get the overflow/underflow status flags
during execution. Of course, you could try to avoid overflow/underflow
by checking the operands rigorously before the operation, but it often
gets too complicated and expensive. In addition, the standard library
throws overflow_error in bitset::to_ulong() or bitset::to_ullong() if
the integral value cannot be represented in the respective type, which
is clearly a runtime condition.

> 2. std::domain_error and std::invalid_argument
> We came up with a distinction saying that a domain_error is thrown
> when a value does not belong to the "static" domain of a function,
> e.g. a function that only works in positive numbers.
> std::invalid_argument should be thrown when a value cannot be used
> because the current state of the program permits that, e.g. putting
> a value twice into an object that holds unique values. This
> container could hold all values of a given type, e.g. string, but
> the domain changes when values are inserted.

It's hard to guess, but let's give it a try. The C++03 standard
library never uses domain_error, and the only occasion where
invalid_argument is used is when bitset constructor gets a string
portion that contains an invalid character (neither '0' nor '1'). I
would say the condition doesn't depend on the program state but is a
"static" domain violation (there's nothing dynamic in the set {'0',
'1'}), so the standard library's usage doesn't seems to agree with
your distinction. Furthermore, I would consider an error that depends
on the current program state as a runtime error.

You could say that an invalid argument is a domain error, or vice
versa. (You could say that an out-of-range argument is an invalid
argument, or it is a domain error, too.) The standard is really
underspecified here. Having said that, my guess is that domain_error
is for domain errors in a more mathematical sense, i.e. sqrt(x) or
log(x) for negative x, and invalid_argument for other more general
cases, e.g. a null pointer given to an argument required to be
non-null, an iterator pointing to a wrong container, an invalid second
argument to std::fopen, etc.

> 3. std::range_error
> I have no idea under what conditions a std:range_error should be
> thrown and not a domain_error, out_of_range_error, overflow_error,
> underflow_error. The only two functions throwing range_error are
> wstring_convert::from_bytes and wstring_convert::to_bytes, but I
> don't see why these two should not throw another exception.

They seem to be conversion errors, and curiously, other conversion
errors in the sto* family throw invalid_argument instead. I don't know
why.

> 4. std::out_of_range and std::length_error seem to be a special case
> of an invalid argument, so why is this not a subclass of
> std::invalid_argument?

I have no idea, either. Maybe the committee didn't want an exception
class hierarchy to be too deep.

> 5. std::regex_error
> This class is thrown when a string is not a valid regex, which is a
> domain error, but it is derived from std::runtime_error. It is also
> thrown when the state machine cannot be constructed due to illegal
> characters, which also seems more like a domain error than something
> outside the scope of the program.

I'm really curious here. What's the difference of constructing a
bitset with an invalid string and constructing a regex with an invalid
string? Why does one throw a logic_error and the other a
runtime_error?

> 6. std::bad_weak_ptr and std::bad_function_call
> These two class are directly derived from std::exception instead of
> std::logic_error. I think that is so because these present severe
> bugs in your program and not exceptions.

Maybe. Similarly for bad_exception, bad_cast, bad_typeid, etc, but I'm
not sure if they are necessarily severe bugs, nor if those under
logic_error are necessarily less severe.

--
Seungbeom Kim

Zeljko Vrba

unread,
May 11, 2012, 6:45:07 AM5/11/12
to
On 2012-05-10, Joshua Maurice <joshua...@gmail.com> wrote:
>
> Code will have mistakes, and the question is what to do in the face of
> the mistakes. The wrong side says just write perfect code. The right
>
"Wrong" :-) You have to (strive to) write perfect code if the end product is
to be linked in the same address space (e.g., C library, math library, etc).
For the sake of intellectual experiment, how would you write fault-tolerant
strchr(), or equivalent for std::string?

>
> PS: Sometimes it makes sense to me to return an error code for an
> unexpected condition. Then again, this is just semantic quibbling.
> What is an unexpected condition? Is a sanity check assert checking for
>
I would argue that unexpected conditions are all errors that might
conceivably happen but that you don't want to or don't know how to handle at
the point of failure. Conditions that make it _impossible_ for the current
path to fulfill its postconditions.

For example: write() or close() of a file failing, getting an error while
acquiring/releasing a mutex, etc. So instead of writing

close(fd);

and hoping all goes well (and never knowing when something goes wrong!),
you could write

if (close(fd) < 0) throw std::runtime_error("close()");

Postcondition was (presumably) properly formatted file, but if close() has
failed, you cannot guarantee that, hence -- throw exception.

But this perspective is incompatible with RAII: typically, you would wrap a
file or mutex in a class whose destructor would release the resource, but
throwing from a destructor is a big no-no. So we have a mechanism for
reporting broken invariants (exceptions), but it is unusable if proper
destruction (e.g., successfully closed file or released mutex) is among
them.

Which gives? Install a custom terminate handler and possibly longjmp()
out of it?

Daniel Krügler

unread,
May 11, 2012, 3:25:18 PM5/11/12
to
Am 10.05.2012 23:28, schrieb Wil Evers:
>>> The problem with std::runtime_error is that it holds a std::string
>>> to store the error message, which implies you cannot safely throw
>>> one when running out of resources.
>>
>> Almost. You cannot safely throw one when running out of memory. But
>> when that happens std::bad_alloc tends to get thrown.
>
> I wouldn't bet on that. At least for C++03, both the expert
> community and some common C++ implementations disagreed on what is
> supposed to happen when the program runs into an exception while
> initializing the object carrying the value of a throw-expression.
> Some thought this should result in a call to terminate(), while
> others advocated propagating the exception that caused the
> initialization of the exception object to fail.

For C++11 the wording has been strongly improved, see especially the
wording changes for

http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1168
http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#475

The rare way of realizing a terminate is now when an exception object
with potentially throwing copy/move-constructor is caught by value.

Note also that the wording for the new std::exception_ptr is carefully
drafted to ensure that a creating such an object (e.g. via
std::current_exception() or make_exception_ptr()) has to cope with
out-of-memory situations and for such situations an object of type
std::bad_alloc is thrown. If an exception is thrown during copying of
the actual exception object, the thrown object is wrapped, or, if that
would not be possible, an onject of std::bad_exception.

In addition the library specification has been improved: As described
by [exception] p2:

"Each standard library class T that derives from class exception shall
have a publicly accessible copy constructor and a publicly accessible
copy assignment operator that do not exit with an exception."

HTH & Greetings from Bremen,

Daniel Krügler

Martin B.

unread,
May 11, 2012, 5:29:22 PM5/11/12
to
On 11.05.2012 01:22, Joshua Maurice wrote:
> On May 10, 11:48 am, Agents Marlow<marlow.age...@gmail.com> wrote:
>> On May 9, 11:03 pm, Wil Evers<boun...@dev.null> wrote:
>>
>>> First, there is 'logic_error', which is based on the belief that
>>> it makes sense to use exceptions to report errors in the program's
>>> logic. C++ has a much better way of dealing with logic errors:
>>> emit a diagnostic and abort.
>>
>> That is not acceptable in some situations. I worked on a very large
>> server app where each function offered by the server was relatively
>> isolated. With your approach a logic error in one server function
>> would bring down the entire server and severe the connection to all
>> its clients. So that our function never did this we used an
>> exception that meant 'an assertion has failed'. It inherited from
>> std::logic_error. We caught it at the entry to our function, where
>> we reported it then exited our function. Users would report that
>> their command failed but the server itself did not fail.
>
> This is a holy war of sorts, but I'll throw-in by saying the
> following:
>
> If you have checks in your code that "makes sure" that your code
> obeys certain invariants, and those checks fail, then something very
> bad has happened. The default action in such a situation is to die
> and dump core. (...)

One problem I see here is that the C++ standard has nothing to say
about "dumping core" and it would appear that terminate()/abort() and
their variants are a very weak substitute.

>
> If this contradicts up-time guarantees that your product needs, then
> the solution is to introduce fault tolerance. Fault tolerance is the
> ability to recover from faults or to continue running in the face of
> faults, where "fault" means: unexpected conditions or errors.
>
> The holy war is how to achieve fault tolerance and exactly what
> constitutes a "fault".
>
> One side, the wrong side, says you should achieve fault tolerance by
> guaranteeing that faults aren't fatal and by trying to minimize
> faults in your code. Play nice. Don't dump core. Don't
> SIGSEGV. Instead of dumping core, throw an exception, return an
> error code, log an error, etc.
>
> The other side, my side, the right side, says that you should
> introduce fault isolation and redundancy to achieve fault tolerance.
> Fault isolation is the ability for one piece of code to be
> unaffected by the faults of another piece of code. (...)

The mistake is to assume that there is a right and a wrong side. What
is right and what is wrong (within reason) is as much a business
decision as a technical one.

Should my "assertions" (quotes deliberate) dump core? Certainly! They
*should*. Whether they *actually* do, or I throw a logical error
instead, is based upon customer expectations and what the app does.

To give you a real real world example: A code base I worked upon
(Windows) wouldn't even deliberatley dump core on access violations
(NULLptr access etc.). It used the Windows sepcific mechanism to turn
those into exceptions, caught them by `catch(...)` and tried a
controlled shutdown of the system. This worked 90% of the time, 9,9%
of the time the process crashed anyway and 0,1% of the time really
weird things happened. Had they dumped core always, this would have
meant that a major part of the 90% incidents would have meant damaged
hardware equipment and significant additional cost to the customers.

Was this system not designed well enough? Maybe. But it *worked* well
enough to be economically viable.

cheers,
Martin

--
Good C++ code is better than good C code, but
bad C++ can be much, much worse than bad C code.

Wil Evers

unread,
May 13, 2012, 12:42:54 AM5/13/12
to
Martin B. wrote:

> Should my "assertions" (quotes deliberate) dump core? Certainly!
> They *should*. Whether they *actually* do, or I throw a logical
> error instead, is based upon customer expectations and what the app
> does.
>
> To give you a real real world example: A code base I worked upon
> (Windows) wouldn't even deliberatley dump core on access violations
> (NULLptr access etc.). It used the Windows sepcific mechanism to
> turn those into exceptions, caught them by `catch(...)` and tried a
> controlled shutdown of the system. This worked 90% of the time, 9,9%
> of the time the process crashed anyway and 0,1% of the time really
> weird things happened. Had they dumped core always, this would have
> meant that a major part of the 90% incidents would have meant
> damaged hardware equipment and significant additional cost to the
> customers.
>
> Was this system not designed well enough? Maybe. But it *worked*
> well enough to be economically viable.

In my opinion, there is nothing inherently wrong with initiating a
"controlled shutdown" when a program discovers a logic error. Such a
controlled shutdown should try to emit a diagnostic, and could attempt
to record some of the program's state - which can be helpful when
trying to figure out happened, or to recover some data that would
otherwise be lost. As long as the user is cleary informed about the
program's failure, and it does not attempt to muddle on, I'd say that
this is perfectly reasonable behavior.

Whether or not an exception should be used to transfer control to the
point where the "controlled shutdown" is initiated, is another matter.
Throwing an exception triggers stack unwinding, which could easily
make things worse than they already were when the logic error was
first detected. A direct call to the controlled shutdown routine
probably has a higher chance of succeeding in trying to limit the
damage.

- Wil


--

Martin B.

unread,
May 13, 2012, 8:18:39 PM5/13/12
to
On 13.05.2012 06:42, Wil Evers wrote:
> Martin B. wrote:
>
>> Should my "assertions" (quotes deliberate) dump core? Certainly!
>> They *should*. Whether they *actually* do, or I throw a logical
>> error instead, is based upon customer expectations and what the app
>> does.
>>
>> To give you a real real world example: A code base I worked upon
>> (Windows) wouldn't even deliberatley dump core on access violations
>> (NULLptr access etc.). It used the Windows sepcific mechanism to
>> turn those into exceptions, caught them by `catch(...)` and tried a
>> controlled shutdown of the system. This worked 90% of the time, 9,9%
>> of the time the process crashed anyway and 0,1% of the time really
>> weird things happened. Had they dumped core always, this would have
>> meant that a major part of the 90% incidents would have meant
>> damaged hardware equipment and significant additional cost to the
>> customers.
>>
>> Was this system not designed well enough? Maybe. But it *worked*
>> well enough to be economically viable.
>
> In my opinion, there is nothing inherently wrong with initiating a
> "controlled shutdown" when a program discovers a logic error. Such a
> controlled shutdown should try to (...)
> As long as the user is cleary informed about the
> program's failure, and it does not attempt to muddle on, (...)
>
> Whether or not an exception should be used to transfer control to the
> point where the "controlled shutdown" is initiated, is another matter.
> Throwing an exception triggers stack unwinding, which could easily
> make things worse than they already were when the logic error was
> first detected. A direct call to the controlled shutdown routine
> probably has a higher chance of succeeding in trying to limit the
> damage.
>

Agreed. We've switched to direct shutdown for lots of code. Some old
code where nothing ever was changed still uses the `catch(...)` approach
and it's really annoying analyzing the process dumps of exceptions where
the stack was unwound. (Windows: A proc dump is written with the exc
record of the thrown exception - but the stack of the origin is already
unwound.) Makes for some surreal post mortem debugging experiences in
WinDbg. :-)

cheers,
Martin

--
Good C++ code is better than good C code, but
bad C++ can be much, much worse than bad C code.


Mathias Gaunard

unread,
May 15, 2012, 1:20:47 PM5/15/12
to
On May 14, 2:18 am, "Martin B." <0xCDCDC...@gmx.at> wrote:

> Agreed. We've switched to direct shutdown for lots of code. Some old
> code where nothing ever was changed still uses the `catch(...)` approach
> and it's really annoying analyzing the process dumps of exceptions where
> the stack was unwound. (Windows: A proc dump is written with the exc
> record of the thrown exception - but the stack of the origin is already
> unwound.) Makes for some surreal post mortem debugging experiences in
> WinDbg. :-)

You should not catch exceptions when you want to debug them.
A good way to deal with this is to add an option to run your unit
tests or whole application in debug mode.


--

Martin B.

unread,
May 16, 2012, 1:37:50 PM5/16/12
to
On 15.05.2012 19:20, Mathias Gaunard wrote:
> On May 14, 2:18 am, "Martin B."<0xCDCDC...@gmx.at> wrote:
>
>> Agreed. We've switched to direct shutdown for lots of code. Some old
>> code where nothing ever was changed still uses the `catch(...)` approach
>> and it's really annoying analyzing the process dumps of exceptions where
>> the stack was unwound. (Windows: A proc dump is written with the exc
>> record of the thrown exception - but the stack of the origin is already
>> unwound.) Makes for some surreal post mortem debugging experiences in
>> WinDbg. :-)
>
> You should not catch exceptions when you want to debug them.
> A good way to deal with this is to add an option to run your unit
> tests or whole application in debug mode.
>

I wrote "post mortem" debugging. By this I meant "debugging" (if you can
call it that) the process/core dump the crashed application produced,
that is, viewing the crash dump in WinDbg. Since the exception record of
the "access violation" that caused the crash is set as active exception
record in the crash dump, the debugger will show the stack where the
exception was thrown ... except that it is already unwound and all local
objects show bogus (post d'tor) values.

cheers,
Martin

--
Good C++ code is better than good C code, but
bad C++ can be much, much worse than bad C code.


Gene Bushuyev

unread,
May 17, 2012, 9:03:54 PM5/17/12
to
On May 11, 3:45 am, Zeljko Vrba <mordor.nos...@fly.srk.fer.hr> wrote:
> On 2012-05-10, Joshua Maurice <joshuamaur...@gmail.com> wrote:
>
> I would argue that unexpected conditions are all errors that might
> conceivably happen but that you don't want to or don't know how to handle at
> the point of failure. Conditions that make it _impossible_ for the current
> path to fulfill its postconditions.
>
> For example: write() or close() of a file failing, getting an error while
> acquiring/releasing a mutex, etc. So instead of writing
>
> close(fd);
>
> and hoping all goes well (and never knowing when something goes wrong!),
> you could write
>
> if (close(fd) < 0) throw std::runtime_error("close()");
>
> Postcondition was (presumably) properly formatted file, but if close() has
> failed, you cannot guarantee that, hence -- throw exception.
>
> But this perspective is incompatible with RAII: typically, you would wrap a
> file or mutex in a class whose destructor would release the resource, but
> throwing from a destructor is a big no-no. So we have a mechanism for
> reporting broken invariants (exceptions), but it is unusable if proper
> destruction (e.g., successfully closed file or released mutex) is among
> them.

I agree that the errors in destructors is a difficult subject for
which C++ standard doesn't offer any good mechanism to deal with. I
would like to hear the opinions on this subject. What practical ways
do we have to deal with errors in destructors? Here are some
possibilities I can think of and their deficiencies.

1) Throwing an exception - not a viable solution for many reasons
2) Completely suppressing an exception in catch(...) - errors become
hidden (you can't rely on logging) and it still doesn't guarantee the
rest of the state is not affected;
3) Fork the process and call terminate in one creating dump, and
suppress all exceptions in the other - non-standard, boilerplate code
in every throwing destructor.

In light of these difficulties, would it be better if the standard in
case of an exception thrown during stack unwinding instead of calling
std::terminate created a nested_exception and re-thrown it continuing
stack unwinding? Then one would be able to install a handler for
std::terminate or catch the exception elsewhere and unravel the nested
exceptions.

Wil Evers

unread,
May 18, 2012, 8:28:06 PM5/18/12
to
Gene Bushuyev wrote:

> On May 11, 3:45 am, Zeljko Vrba <mordor.nos...@fly.srk.fer.hr>
> wrote:
>>
>> For example: write() or close() of a file failing, getting an error
>> while acquiring/releasing a mutex, etc. So instead of writing
>>
>> close(fd);
>>
>> and hoping all goes well (and never knowing when something goes
>> wrong!), you could write
>>
>> if (close(fd) < 0) throw std::runtime_error("close()");
>>
>> Postcondition was (presumably) properly formatted file, but if
>> close() has failed, you cannot guarantee that, hence -- throw
>> exception.
>>
>> But this perspective is incompatible with RAII: typically, you
>> would wrap a file or mutex in a class whose destructor would
>> release the resource, but throwing from a destructor is a big
>> no-no. So we have a mechanism for reporting broken invariants
>> (exceptions), but it is unusable if proper destruction (e.g.,
>> successfully closed file or released mutex) is among them.
>
> I agree that the errors in destructors is a difficult subject for
> which C++ standard doesn't offer any good mechanism to deal with. I
> would like to hear the opinions on this subject. What practical ways
> do we have to deal with errors in destructors?

I wonder if that's the right question. Another one would be: what is
the reason we need to deal with errors in destructors in the first
place?

For example, consider the call to close() above. The Linux manual
page for close() comments:

Not checking the return value of close() is a common but
nevertheless serious programming error. It is quite possible that
errors on a previous write(2) operation are first reported at the
final close(). Not checking the return value when closing the file
may lead to silent loss of data. This can especially be observed
with NFS and with disk quota.

In other words, close() behaves as if it both performs some I/O (which
can fail for reasons beyond the control of the program) *and*
deallocates a system resource (which should never fail). In the first
case, we may want to report an error by throwing an exception; in the
second case, we need to fix the program.

It is close()'s surprising double-duty nature that causes the problem
we're discussing here. Destructors are compiler-generated callbacks
that tell us to release our resources; they were simply not designed
for anything else.

Obviously, as a practical matter, it is possible to design a class
that allows users to explicitly request an exception on I/O errors
reported by close():

class file {
public :
// constructors, read(), write(), and...

void close(); // throws on I/O errors

~file(); // closes implictly if still open; never throws
};

void save()
{
file f(/* args */);

// gather and write data, and then...

f.close();
}

Here, closing the file will only result in an exception if an I/O
error is reported *and* we're not unwinding because of some other
exception.

- Wil

Zeljko Vrba

unread,
May 19, 2012, 3:46:03 PM5/19/12
to
On 2012-05-19, Wil Evers <bou...@dev.null> wrote:
>
> It is close()'s surprising double-duty nature that causes the
> problem we're discussing here. Destructors are compiler-generated
> callbacks that tell us to release our resources; they were simply
> not designed for anything else.
>

In principle, I agree, but you may find examples where nontrivial work
is done in destructors, for example, unlocking a mutex.

So it seems that destructors are usable only for doing work that
cannot fail, which is, in practice, just freeing memory. (An
alternative is to ignore errors in destructors and hope for the best
-- as with close and mutex examples.)

Maybe the common wisdom of not throwing from a destructor is wrong --
why is everybody so afraid of terminate() being called when a custom
terminate handler can be installed?

Wil Evers

unread,
May 19, 2012, 8:22:37 PM5/19/12
to
Zeljko Vrba wrote:

> On 2012-05-19, Wil Evers <bou...@dev.null> wrote:
>>
>> It is close()'s surprising double-duty nature that causes the
>> problem we're discussing here. Destructors are compiler-generated
>> callbacks that tell us to release our resources; they were simply
>> not designed for anything else.
>>
>
> In principle, I agree, but you may find examples where nontrivial
> work is done in destructors, for example, unlocking a mutex.
>
> So it seems that destructors are usable only for doing work that
> cannot fail, which is, in practice, just freeing memory. (An
> alternative is to ignore errors in destructors and hope for the best
> -- as with close and mutex examples.)

No, that's not what I tried to say. I'm arguing that, at least in
principle, destructors should not rely on operations that can fail
for reasons beyond the control of the program.

For example: it is a bad idea to allocate memory in a destructor,
unless of course you're prepared to handle an out-of-memory situation
locally, without an exception escaping.

On the other hand, unlocking a mutex in a destructor is perfectly
fine, because that should only fail because of an error in the program
itself. Furthermore, it makes little sense to report failure to
unlock a mutex with an exception; your best bet is probably to report
the error and give up immediately.

> Maybe the common wisdom of not throwing from a destructor is wrong
> -- why is everybody so afraid of terminate() being called when a
> custom terminate handler can be installed?

Well, for one thing, even a user-defined terminate handler is not
allowed to return control to its caller; it must terminate the
execution of the program.

- Wil

Zeljko Vrba

unread,
May 20, 2012, 10:52:03 AM5/20/12
to
On 2012-05-20, Wil Evers <bou...@dev.null> wrote:
>
> Well, for one thing, even a user-defined terminate handler is not
> allowed to return control to its caller; it must terminate the
> execution of the program.
>
Exactly. Exceptions report broken invariants, and a double-exception
during stack unwinding is an irreparable breakage of invariants. What
else is there than to terminate the program? With a custom terminate
handler, you have the chance to save the program's interim state and
maybe even fork() and reexec yourself before the terminating instance
finally exits. You don't even need to fork first; exec will
reinitialize the address space and start main() from scratch, but open
files etc. will be preserved.

Martin B.

unread,
May 20, 2012, 10:59:36 AM5/20/12
to
{ Please don't SHOUT; thanks! -mod }

On 19.05.2012 21:46, Zeljko Vrba wrote:

> On 2012-05-19, Wil Evers<bou...@dev.null> wrote:
>>
>> It is close()'s surprising double-duty nature that causes the
>> problem we're discussing here. Destructors are compiler-generated
>> callbacks that tell us to release our resources; they were simply
>> not designed for anything else.
>>
>
> In principle, I agree, but you may find examples where nontrivial work
> is done in destructors, for example, unlocking a mutex.
>
> So it seems that destructors are usable only for doing work that
> cannot fail, which is, in practice, just freeing memory. (An
> alternative is to ignore errors in destructors and hope for the best
> -- as with close and mutex examples.)

Failing to unlock a mutex is either impossible (e.g. Win32
LeaveCriticalSection returns void -
http://msdn.microsoft.com/en-us/library/windows/desktop/ms684169%28v=vs.85%29.aspx)
or a severe program bug (e.g. pthread_mutex_unlock will only fail if
it was already unlocked or is owned by a different thread).

The thing with `close()`, be it some file thingy or a DB connection or
whatever, is that it's a bit of a brittle design to begin with when
you implicitly "commit" something from a destructor.

The most practical solution I'd see would be to be able to detect in
the dtor whether it is safe to throw the exception and based on this
information we could add additional logic to determine course of
action. (throw anyway, try logging and ignore otherwise, call
terminate directly, etc.)

As far as I remember though, IT IS NOT POSSIBLE TO SAFELY DETERMINE in
the destrctor whether it's "safe" to (re)throw an exception from
close(). Has anything changed in C++11 in this regard??

cheers,
Martin


--
Good C++ code is better than good C code, but
bad C++ can be much, much worse than bad C code.


Wil Evers

unread,
May 20, 2012, 7:09:22 PM5/20/12
to
Martin B. wrote:

> As far as I remember though, IT IS NOT POSSIBLE TO SAFELY DETERMINE
> in the destrctor whether it's "safe" to (re)throw an exception from
> close(). Has anything changed in C++11 in this regard??

I don't think so.

- Wil


--

James K. Lowden

unread,
May 25, 2012, 12:22:26 AM5/25/12
to
On Fri, 18 May 2012 17:28:06 -0700 (PDT)
Wil Evers <bou...@dev.null> wrote:

> It is close()'s surprising double-duty nature that causes the problem
> we're discussing here. Destructors are compiler-generated callbacks
> that tell us to release our resources; they were simply not designed
> for anything else.

Exceptions don't exist to tell us to free resources. Exceptions exist
to report problems. They're a big step forward from simply returning
an error code and trusting every caller to DTRT.

I don't see anything special about performing, or failing to perform,
I/O in a destructor. I don't see what all the fuss is about. Any I/O
can fail, and any exception can be caught. Or, if not caught, at least
reported. And, hopefully, seen.

> For example: it is a bad idea to allocate memory in a destructor,
> unless of course you're prepared to handle an out-of-memory situation
> locally, without an exception escaping.

And yet the standard exception classes allocate memory. Even if they
didn't, what's to assure there's room on the stack to instantiate the
object to be thrown? What about a failing drive, an uncorrected memory
error, flaky power supply, or any other of myriad improbable,
unavoidable, unrecoverable failure conditions? What about a blackout
that wipes out the whole east coast of the United States for the better
part of a week?

In the great scheme of things ISTM failing to allocate a string on the
heap is the least of our worries.

Unless you have total control over all the software in your system *and*
all the software surrounding it -- surely the exception these days -- a
low memory situation will trigger a plethora of bugs in mostly
untested code. The odds of landing on its feet are roughly nil.

Which is fine, sort of, because it has to be. It's only software. The
consequences of ending in a bad way are merer than getting a bad
haircut. Because hair, after all, unlike machine state, is
nonrecoverable and cannot be duplicated.

--jkl

Wil Evers

unread,
May 27, 2012, 9:01:32 PM5/27/12
to
James K. Lowden wrote:

> On Fri, 18 May 2012 17:28:06 -0700 (PDT)
> Wil Evers <bou...@dev.null> wrote:
>
>> It is close()'s surprising double-duty nature that causes the
>> problem we're discussing here. Destructors are compiler-generated
>> callbacks that tell us to release our resources; they were simply
>> not designed for anything else.
>
> Exceptions don't exist to tell us to free resources.

I did not say so. I said *destructors* exist to tell us to free
resources.

> Exceptions exist to report problems. They're a big step forward
> from simply returning an error code and trusting every caller to
> DTRT.

I agree.

> I don't see anything special about performing, or failing to
> perform, I/O in a destructor. I don't see what all the fuss is
> about. Any I/O can fail, and any exception can be caught. Or, if
> not caught, at least reported. And, hopefully, seen.

The special thing about the code in a destructor is that it can't
reliably report failure by throwing an exception. In C++03, an
exception escaping from a destructor invoked from the stack unwinding
machinery results in a call to terminate(). In C++11, destructors are
noexcept by default; unless special action is taken, terminate() will
also be called when an exception escapes from a destructor invoked as
part of the normal flow of control.

So we're left with a choice. We can use some other error reporting
mechanism, but that would mean we are back to relying on our caller to
to discover the error and act accordingly. Alternatively, we can try
to restrain ourselves and only use destructors for things that cannot
reasonbly fail. That is my preference.

- Wil

Edward Rosten

unread,
May 28, 2012, 1:12:53 PM5/28/12
to
On May 28, 3:01 am, Wil Evers <boun...@dev.null> wrote:

> So we're left with a choice. We can use some other error reporting
> mechanism, but that would mean we are back to relying on our caller to
> to discover the error and act accordingly. Alternatively, we can try
> to restrain ourselves and only use destructors for things that cannot
> reasonbly fail. That is my preference.

I don't see that as an alternative, unless I've misunderstood. If you
don't put close() in the destructor, you rely on the caller of the
class both remembering to destroy it properly and checking for errors
when it has done so.

I think the problem is that sometimes failure can lead to cascading
errors and how to deal with that is not necessarily obvious.
Exceptions mostly make the usual, simple case trivial.

There is, of course the other problem is that not close()ing a file in
a destructor is likely to lead to far more errors than double throws
caused by close() failing.

Wil Evers

unread,
May 28, 2012, 8:52:03 PM5/28/12
to
Edward Rosten wrote:

> On May 28, 3:01 am, Wil Evers <boun...@dev.null> wrote:
>
>> So we're left with a choice. We can use some other error reporting
>> mechanism, but that would mean we are back to relying on our caller
>> to to discover the error and act accordingly. Alternatively, we
>> can try to restrain ourselves and only use destructors for things
>> that cannot reasonbly fail. That is my preference.
>
> I don't see that as an alternative, unless I've misunderstood. If
> you don't put close() in the destructor, you rely on the caller of
> the class both remembering to destroy it properly and checking for
> errors when it has done so.
>
> I think the problem is that sometimes failure can lead to cascading
> errors and how to deal with that is not necessarily obvious.
> Exceptions mostly make the usual, simple case trivial.
>
> There is, of course the other problem is that not close()ing a file
> in a destructor is likely to lead to far more errors than double
> throws caused by close() failing.

As I said elsewhere in this thread, it is possible to equip the
class holding the file descriptor with an explicit close() member
function that will throw an exception if the close() system call
reports an I/O error. The class's destructor would check to see if
the descriptor is still open, and close it if it is, without checking
for any I/O errors. That would prevent a descriptor leak, as well as
a double fault if the destructor is invoked when unwinding the stack.

Obviously, this essentially just redefines the problem by simply
ignoring I/O errors when the close() system call is invoked from the
destructor, and it relies on users interested in these errors to do
the right thing. However, given the way the close() system call is
defined, this is probably the best we can do.

- Wil

Dave Harris

unread,
May 31, 2012, 12:25:32 AM5/31/12
to
edward...@gmail.com (Edward Rosten) wrote (abridged):
> I don't see that as an alternative, unless I've misunderstood. If
> you don't put close() in the destructor, you rely on the caller of
> the class both remembering to destroy it properly

Your class's destructor can check whether close() was called, and
assert if it wasn't. So the caller's mistake will be detected the
first time the code is executed. This ought to be enough in practice,
if they have a testing regime.

(If you believe in defensive programming, you can include some of that
in the destructor too: close the file, log the error, whatever.)

> and checking for errors when it has done so.

You can expose close() through a member function that throws on error,
if that's appropriate.

-- Dave Harris, Nottingham, UK.
0 new messages