Re: Exception handling and encapsulation
benben <benhonghatgmaildotcom@nospam> wrote in
Hello C++ experts,
This is rather a design question, I must say. My question is, how do
you write exception safe code while ensuring encapsulation?
Wow, this is a nicely open-ended question. :) I think the easiest
answer is that exceptions are classes and as such can participate in an
inheritance heirarchy. Since this is C++, you can even use multiple
inheritance. So, what can this mean to encapsulation? Most simply, if
you derive your exceptions from one of the std:: heirarchy of exceptions
(std::exception, std::runtime_error, std::logic_error etc) then you
higher level classes can just catch those and won't care about the
actual exception. If you start to let your creative juices flow a bit,
then you can create your own type tags which can suggest a strategy for
handling the exception, or categories for the exception so that higher
level code doesn't care about the exact exception, but rather the
inherited type which can suggest a strategy for handling it. Remember
that catch clauses can not only catch the specific class being thrown,
but also any inherited classes. For example. if you have some classes
like 'reestablish_connection' or 'wait_and_retry' then you can inherit
from one or more of those and have your high level code catch those
instead of the specific exception. The high level code can then either
catch the specific exception and do some appropriate action or bail.
Another possibility is to define you own interface for the exception
such that higher level code can pull out the pieces of information they
need to take action, but still don't need to know the exact type of the
exception thrown. That is you can make the base class of your exception
understand GetErrorCode() or something to allow the handlers of the
exception the ability to deal with problems. If you inherit from
std::exception, you get the what() method that can return a string. The
string can either be a real message or just a small handle into another
lookup table. However you want to use it.
The point of the above is to just get you thinking about possibilities.
The fact that exceptions are classes themselves gives you the ability to
make the full use of that power. With the full gamut of muliple
inheritance and interfaces, the choices are pretty endless.
In C++, if a function by itself does not know how to handle an
exception raised from a lower level system, we usually implement the
function in such a way that it is exception safe, yet allow the
exception to be propagated to a higher level system.
However, since each system level defines a certain abstraction,
allowing exceptions to propagate through multiple system levels may
breach encapsulation, because it exposes low level implementation
details. The exception hence can be less useful than it should be, as
the higher level system will have little knowledge how such low level
exceptions are to be handled accurately.
Furthermore, should the higher level system choose to handle
exceptions from multiple layers down, it risks to be turned invalid as
soon as changes are made to the lower level systems. This is very
similar to accessing a class member that is ought to be private.
To give an example, consider the following program, which consists of
three distinct system levels: at the lowest, a database class; up one
level, an enrolment form; and at the highest level, the main function
that uses the enrolment forms.
// Incomplete program, will not link.
// Low level system
void connect(std::string location) throw (connection_error);
void create_record(std::string rc) throw (IO_error,
These exeception specification generally don't do what people (except
for experts) expect. I think people expect this to be a declaration
telling the compile to flag a compilation error if the method can throw
something other than the spelled out exceptions. That isn't what
happens. What happens is that the compiler generates code to cause any
exception but the ones specified to invoke unexpected() which isn't
allowed to return, but can throw something. This is not generally the
behavior you want.
Here is the dilemma:
1) If main() is to handle the exceptions from submit1() member
function call, it will have to deal with exceptions from both
enrolment_form (mid-level) and database (low-level.) Since the use of
a database is to be encapsulated from main(), the writer of main()
obviously knows little how to handle such exceptions.
2) If the mid-level enrolment_form class is to catch every lowe-level
exceptions and translate it to one of its own (but higher level in
abstraction) exception types, such as submission_error, encapsulation
is enforced but:
a. The writer of main() will be equally clueless on how exceptions,
such as submission_error shall be handled, since the nature of the
exception is abstracted away (that is, the type information of the
original exception, at least)
b. The implementation of mid-level system, enrolment_form, is
complicated as it is responsible to translate all lower-level
exceptions. For the very least, functions are littered with try-catch
blocks, which is ugly IMO.
The idea would be to have all your exceptions derive from std::exception
or other class of your choosing and have the higher level guys catch
those exceptions instead of the specific ones. Define your own heirarchy
of exception classes that your high level code can handle. Then derive
your specific exceptions from one or more of those. You can then maintain
encapsulation and yet do some very interesting recovery strategies.
Hope that helps in some way.