Boost logo

Boost :

From: Oleg Abrosimov (beholder_at_[hidden])
Date: 2006-07-17 01:06:25


Beman Dawes wrote:
> "Oleg Abrosimov" <beholder_at_[hidden]> wrote
>> Hello Beman!
>>
>> The N1975 proposal has a section called "Important Design Decisions". It
>> is a very good place to put a rational why you've chosen the error
>> reporting strategy with system_error.
>>
>> currently, it has only one sentence "Because filesystem operations often
>> encounter unexpected runtime errors, the library by default reports
>> runtime errors via C++ exceptions, and ensures enough information is
>> provided for meaningful error messages, including internationalized
>> error messages."
>>
>> It is not enough to understand why you don't follow the way of other
>> portable file system frameworks. For example, in java language there are
>> IOException and its descendants like FileNotFoundException. It can be
>> said that it follows the usual OO-exception handling mantra - define a
>> hierarchy of exception classes and use them to report error conditions
>> from your functions. Furthermore, it can be said that it is a way to
>> abstract low-level system errors and provide only high level logical
>> errors (or exceptions).
>
> Yes, I'm well aware of that approach and was one of the participants in LWG
> at the time that approach was designed back in the early 1990's.

It seems that you are the right person to ask my questions. See below.

> A strength
> of such a high level approach is that it abstracts away a lot of differences
> between operating systems. But sometimes programmers need to deal with those
> lower level details. The set of error conditions that has to be dealt with
> is also open ended, and exception hierarchies where there is one exception
> for each error type have trouble dealing with that. There is also the issue
> of interface bloat - there are a very large number of potential errors, and
> having an exception type for each one of them become unwieldy.

There is also one more issue on client side. If library provides many
exception classes that can be thrown and user is interested in many of
them ( he can not just use catch(lib_base_exception&) {...} trick ) then
code becomes (1) _very_ bloated and (2) unmaintainable:

void some_client_func()
try {
   call_lib_with_many_exceptions();
} catch(exception1& e1) {
   do_response1(e1);
} catch(exception2& e2) {
   do_response2(e2);
} catch(exception3& e3) {
   do_response3(e3);
} catch(exception4& e4) {
   do_response4(e4);
} catch(exception5& e5) {
   do_response5(e5);
} catch(exception6& e6) {
   do_response6(e6);
}

1) code is bloated;
2) this exception handling code is duplicated many times in many of
some_client_func's. It means that every time when exception handling
code should be changed or exception7 handler should be added one needs
to make corrections in many places. It is well known as a "maintenance
hell".

Actually, this bloating problem quickly arises in production code that
tries to use exceptions. Even without libs that publish too many
exception types. (std::string, const char*, CImageException,
std::exception and catch all handlers are the real example from one of
real-world projects I've worked on)

In java language, for example, this problem is avoided because compiler
enforces exception handling in the place where it can be thrown:

int n = 0;
try { n = Integer.parseInt("1"); } catch(NumberFormatException e) {}

it can not be simply written as:

int n = Integer.parseInt("1");

because in this case one must specify NumberFormatException in throws
specification of enclosing function.
As a consequence, it leads to "handle one exception at a time" principle.

In C++ it can not be applied, because every statement can throw and
compiler can't help you with it. As a consequence, one ends up with many
exception handlers that bloats code as was shown above.

Actually, there can be a workaround for the bloating/maintenance
problem. I can imagine a higher-level function that gets a function to
call and something like mpl map where keys are types of exceptions to
handle and values are corresponding exception handlers. In this case my
exception handling code becomes:

void do_response1(exception1& e1);
void do_response2(exception2& e2);
void do_response3(exception3& e3);
void do_response4(exception4& e4);
void do_response5(exception5& e5);
void do_response6(exception6& e6);

typedef ... exception_handlers_map;

void some_client_func() {
   call<exception_handlers_map>(&call_lib_with_many_exceptions);
}

Then when one needs to replace do_response5 handler with more
sophisticated one (and only for one client_func) then he do apply some
mpl algorithm for that and specify the result of this algorithm in
call<...>(...) statement.

This solution looks promising but in practice it would results in code
like this:

void some_client_func_impl(); // provides actual implementation that can
throw many exceptions

void some_client_func() {
   call<exception_handlers_map>(&some_client_func_impl);
}

May be not too bad, just one more trampoline function, but it looks like
a hack that workarounds some deficiency in C++ exception handling
mechanism. Can you please elaborate to improve my understanding of what
this deficiency is and how it can be solved in the first place - in
language itself? Or maybe I've just missed something in C++'s exception
handling machinery?

>
>> It is clear from n1975 that you abandoned the OO approach and fall back
>> to the C-style error reporting and handling. Yes there are exceptions,
>> but no hierarchy. And exception handling is done by dispatching on a
>> base of error codes but not on exception types. So, it is not really far
>> from "C" I believe.
>
> That was the intent. To provide a C++ interface that is higher level than
> the C interface, but not so high level that programmers can't get at the
> details.

consider the following code:

// untested
namespace boost {
namespace win {
class win_specific_error1;
//...
class win_specific_errorN;
}

namespace posix {
class posix_specific_error1;
//...
class posix_specific_errorM;
}
}

All these error classes are inherited from filesystem_error. Then client
can obtain all information in a typesafe manner:

try {
   call_filesystem_lib();
} catch(win_specific_error10& e) {
...
} catch(win_specific_error20& e) {
...
}

Yes, it bloats both, library interface and client exception handling
code, but:
1) library interface is written one time and used many times. For me it
means that a huge amount of exception classes hidden in special
namespaces is not an issue. It doesn't complicates simple usages when
only abstract exceptions like filesystem_error are handled by client and
provides a consistent and typesafe way for those who wants all the low
level information.
2) bloating on the client side was discussed above and can be solved as
was shown. (It needs further discussion though)

>
>> One more point is that type safety is abandoned here too.
>
> Not sure what you mean here. Could you give an example?

try { call_filesystem_lib(); }
catch (filesystem_error& e) {
switch(e.code()) {
   case 1 :
     do_responce1();
     break;
   case 2 :
     do_responce2();
     break;
   default :
     assert(false); // unknown error occurred!
}
}

can you see typesafety here? I can not. For me it is not different from:

int err_code = call_filesystem_lib_from_C();
switch(err_code) {
   case 1 :
     do_responce1();
     break;
   case 2 :
     do_responce2();
     break;
   default :
     assert(false); // unknown error occurred!
}

hope you see my point now.

To conclude:
1) there seems to be a real deficiency in exception handling mechanism
that prevents library authors from providing many useful exception
classes. This problem needs further investigation. in the meantime it
can be workarounded with approach shown in this message (or better one).
2) filesystem error handling machinery reflects the problem mentioned
and falls back to the run-time dispatching based on error code that is
not type safe.

>
> >I should add here that I agree with your decision, but I think that
>> it shouldn't be made implicit, without a really good explanation and
>> rational. The reason is simple: it changes the paradigm that is written
>> in all OO and/or C++ books.
>
> The plan is to take the feedback from Boosters, improve the details of the
> interfaces, and present a proposal to the LWG refining the work that has
> already been accepted. I'll try to add more rationale to that document.
>
> Thanks,
>
> --Beman
>
>
>
> _______________________________________________
> Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
>


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk