B
benben
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?
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.
#include <iostream>
#include <sstream>
// Incomplete program, will not link.
///////////////////////////////////////////////////
// Low level system
class connection_error{};
class IO_error{};
class repetition_error{};
class database
{
public:
void connect(std::string location) throw (connection_error);
void create_record(std::string rc) throw (IO_error,
repetition_error);
// ...
};
///////////////////////////////////////////////////
// Mid level system
class submission_error{};
class invalid_name{};
class invalid_age{};
class enrolment_form
{
std::string name;
unsigned int age;
void check() const
{
if (name.size() == 0)
throw invalid_name();
if (age > 80 || age < 18)
throw invalid_age();
}
public:
enrolment_form(std::string _name,
unsigned int _age);
// submission routine version 1
// throw everything has it
void submit1() const
{
check(); // may throw invalid_name
// or invalid age
database db;
db.connect("enrolment"); // may throw connection_error
std:stringstream oss;
oss << name << ":" << age;
db.create_record(oss.str()); // may throw IO_error
}
// submission routine version 2
// translate lower level exceptions
void submit2() const
try
{
submit1();
}
catch (connection_error)
{
throw submission_error();
}
catch (IO_error)
{
throw submission_error();
}
};
///////////////////////////////////////////////////
// High level
int main()
{
enrolment_form form1("Ben", 22);
form1.submit1();
enrolment_form form2("Bob", 22);
form2.submit2();
}
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.
It can be done though, through snowballing fashion, in which the lower
level exception information is rolled into the translated exception
every time the exception propagates up one abstraction level. The down
side of this solution is it is complicated. It requires exceptions to
have more complex data structure which itself may raise a few terminal
exceptions. Exception types with different conventions can also make
this strategy very difficult, if not at all impossible.
Most of you must have been involved in some large C++ projects dealing
with such problems. I would like to hear what you think, how you deal
with it in your designs, what tools you use to levitate such problem, or
just generally what other options I still have. I would love to find out
a graceful solution to problems of this kind!
Thanks!
Ben
This is rather a design question, I must say. My question is, how do you
write exception safe code while ensuring encapsulation?
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.
#include <iostream>
#include <sstream>
// Incomplete program, will not link.
///////////////////////////////////////////////////
// Low level system
class connection_error{};
class IO_error{};
class repetition_error{};
class database
{
public:
void connect(std::string location) throw (connection_error);
void create_record(std::string rc) throw (IO_error,
repetition_error);
// ...
};
///////////////////////////////////////////////////
// Mid level system
class submission_error{};
class invalid_name{};
class invalid_age{};
class enrolment_form
{
std::string name;
unsigned int age;
void check() const
{
if (name.size() == 0)
throw invalid_name();
if (age > 80 || age < 18)
throw invalid_age();
}
public:
enrolment_form(std::string _name,
unsigned int _age);
// submission routine version 1
// throw everything has it
void submit1() const
{
check(); // may throw invalid_name
// or invalid age
database db;
db.connect("enrolment"); // may throw connection_error
std:stringstream oss;
oss << name << ":" << age;
db.create_record(oss.str()); // may throw IO_error
}
// submission routine version 2
// translate lower level exceptions
void submit2() const
try
{
submit1();
}
catch (connection_error)
{
throw submission_error();
}
catch (IO_error)
{
throw submission_error();
}
};
///////////////////////////////////////////////////
// High level
int main()
{
enrolment_form form1("Ben", 22);
form1.submit1();
enrolment_form form2("Bob", 22);
form2.submit2();
}
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.
It can be done though, through snowballing fashion, in which the lower
level exception information is rolled into the translated exception
every time the exception propagates up one abstraction level. The down
side of this solution is it is complicated. It requires exceptions to
have more complex data structure which itself may raise a few terminal
exceptions. Exception types with different conventions can also make
this strategy very difficult, if not at all impossible.
Most of you must have been involved in some large C++ projects dealing
with such problems. I would like to hear what you think, how you deal
with it in your designs, what tools you use to levitate such problem, or
just generally what other options I still have. I would love to find out
a graceful solution to problems of this kind!
Thanks!
Ben