Alexander Liss
There
are many styles of Error Reporting, Logging and Exception Handling and there
are various facilities supporting them.
One
type of facilities is centralized; it is well known and it has many supporters.
Its benefits in a case of small RAM (when it is desirable to store error
messages separately in a large durable memory) or in a case of operating an
application consisting of a set of processes written in different languages are
obvious. However, here we advocate for localized Error Reporting, Logging and
Exception Handling.
Centralized
approach to Error Reporting, Logging and Exception Handling often simplifies
initial development, because it provides ready
facilities. However, it makes incremental development and especially software
update increasingly more difficult, because updates of Error Reporting, Logging
and Exception Handling require switches to different views of the system in
development and requires coordination with other specialists. This interferes
with developers' focus and introduces mistakes and delays.
Systematic
localized approach simplifies incremental modification of software.
Localized approach starts with software
wrappers.
Often,
a wrapper is a class or a group of classes providing a logically organized,
easy to understand and memorize interface to some functionality related to
hardware, operating system, network etc. This functionality is provided by some
low level libraries, and a wrapper translates their interface.
Any
wrapper adapts functionality provided by low level libraries to development
situations, which it serves. Such adaptation includes selection of a subset of
services recommended for use in these development situations, reorganization of
a set of possible error states and their descriptions.
Usually,
wrappers do not log errors; they provide Error Reporting. In wrappers, there is
a local to wrapper enumerator of error states and a wrapper returns a value
from this enumerator in case of an error. In addition, it provides a
string-description of the error.
Note that sometimes, maintainers of
libraries used by the wrapper add new error states, which are described only in
an error string, which could be extracted in the case of error. This forces a
wrapper developer to define a generic error state, for example
"see_string".
Errors, which happens during initialization
(in constructor), should be reflected in a wrapper's "status".
A Wrapper class looks as:
class Wrapper
{
enum Status
{ok,
… };
enum ErrorState
{none,
see_string, …};
int status;
int error_string(string&, ErrorState );
…
};
Higher
layers of functionality have to provide logging. At this point, developers run
into a problem - different modules of software have to log into the same log
facility.
There
is no universal logging facility (beyond syslog), and developers tend to choose
some logging facility or develop one for a project and use it throughout a
project. This leads to not reusable and not portable code.
It
is better to provide own logging interface for each software module, which
could be implemented using a project's logging facility.
This
could be implemented as follows.
For
a class Aaa, there is a class AaaLogger, which
provides such interface:
class AaaLogger
{
public:
virtual int log_event(const char*)const;
};
class Aaa
{
public:
int set_logger(AaaLogger&);
private:
AaaLogger *logger;
};
In an application, there is a logger class
derived from AaaLogger, where its virtual function log_event()
is implemented using project's logging facility. An instance of this class is
passed to an instance of the class Aaa.
It
could be a dedicated instance of logger class for an instance of the Aaa class. In this case, it is reasonable to create it on
heap and let destructor of Aaa class delete it.
It
could be one instance of a logger class for a few instances of the Aaa class. In this case, this instance should be deleted
independently after all instances of the Aaa class,
which are using it, are deleted.
Different
instances of Aaa class could use different logger
classes.
An
additional benefit of such logging interface is flexibility of modification of
logging related to a particular software module. Often, this modification could
be localized to the implementation of logging interface used by this module.
Exception
Handling is a convenient tool, which is often overused. A problem arises when
the logic of exceptions throwing and catching breaks modular and layered
structure of code.
This
problem is not visible in small applications used to demonstrate Exception Handling, however one becomes painfully aware of it, when
one has to debug a large event driven or multithreaded application. It is such
a big problem that developers of multithreaded applications tend to avoid using
Exception Handling all together. This is not always possible though, because
STL uses Exception Handling.
The
solution is in cautious use of Exception Handling: any exception thrown in a
software module should be caught in the same module. If a class is viewed as a
software module, then its public member functions should not throw exceptions.
If a group of classes is viewed as a software module, then public member
functions of classes representing interface of this group should not throw
exceptions.
Exception
Handling is a convenient tool for dealing with problems occurring in
constructors. However, constructors of classes serving as interface to a
software module should not throw exceptions. Each such class,
should have a special attribute "status", which is checked by an
application immediately after an instance construction.
With
this recommendation, each STL object should be used inside some software
module, which catches all exceptions STL object could throw.