Extensions to the object model -- abnormal events

Instructor's Guide


intro polymorphism idioms patterns events summary, Q/A, literature
When studying the literature on OOP, one can discern a tendency of convergence between the distinct (language) camps. For instance, as already noted, interpreted languages are being optimized to compete with C/C++, and on the other hand, proposals are being made to extend C++ with some of the more exotic features found in, for instance, Smalltalk or Flavors, features such as dynamic typing and meta classes. Also, there is an interesting convergence and mutual influence between Eiffel and C++. For instance, the newer version of Eiffel also supports constructors and overloading, whereas rumors have it that multiple inheritance in C++ has primarily been inspired by its use in Eiffel.

In the following we will briefly explore some extensions to the basic object model, and try to establish why they may be considered useful. See slide 2-extensions.


Meta classes

Object identity

Persistence

Active objects


slide: Extensions to the object model

Meta classes are a feature of Smalltalk supporting the definition of class-wide variables and so-called class methods. Class methods must be used to create new instances of the class. Neither Eiffel nor C++ support meta classes, although C++ supports so-called static data and function members, which are accessible by all instances of a class. The action of creating objects (as instances of classes) is for both languages taken care of by special creation functions or constructors. A feature that is felt to be lacking in C++ is the possibility of obtaining type information during runtime. Although libraries exist that provide the required functionality (to some extent), it seems likely that the language will be extended by constructs supporting this directly. (Currently, the ANSI standardization committee for C++ is studying proposals to introduce features supporting dynamic type information in C++.) See also section OMG. In most object-oriented systems, references to objects are system dependent (addresses in some memory space) and are usually lost when program execution stops. To facilitate debugging it would be convenient to be able to deal with (unique) symbolic references to objects. A more flexible scheme of object naming may also be important for distributed systems, since symbolic identities may also have meaning outside the program's execution space. See section distribution.

Object identity also plays an important role in object-oriented data bases, since the persistent storage of objects requires a naming scheme that allows objects to be written to stable storage and retrieved independently from the current execution.

To my mind, one of the most interesting extensions to the basic object model is the notion of active objects, objects with autonomous behavior. Important issues that relate to this extension are, what concurrency model must be supported and what to do with distribution. See chapter \ref{Distribution and concurrency}. Moreover, from the perspective of design we must ask, are active objects useful and how do they fit in with the design of sequential systems?

Abnormal events

In case the computation proceeds as required, everything is fine. But what should be done when a failure occurs due to memory exhaustion, arithmetical overflow or the violation of an assertion? Must we check for all possible errors or must we rely on separate exception handling mechanisms? In general, an abnormal event occurs when an operation cannot perform its desired computation. See slide 6-abnormal.

Abnormal events

Exceptions in C++

\zline{\fbox{{\em try \& catch}}}
  class Matherr { }
  class Overflow : public Matherr {}
  
  try {
  	f(); 
do some arithmetic
} catch (Overflow) {
// handle Overflow
} catch (Matherr) {
// handle non-Overflow Matherr
}

Assertions

\zline{\fbox{{\em throw}}}
  template< class T, class X >
  inline void Assert(T expr, X x) {
     if (!NDEBUG) if (!expr) throw x;
     }
  

slide: Abnormal events

When an abnormal event occurs, two possible courses of action can be taken. Either the operation can fail and raise an exception (thereby transferring the flow of control to an exception handler) or the operation can invoke a correction routine (that tries to correct the situation that caused the abnormal event) and continue the computation. The latter solution is called an intervention. In contrast to an exception, an intervention does not change the flow of control; it performs the correction and then resumes the operation.

Interventions are familiar from daily computing practice in the form of interrupts, such as are used to kill a process. Under Unix, an interrupt is implemented as a signal that may be associated with a handler. The interrupt handler usually invokes the exit function.

Exceptions, on the other hand, are not (yet) familiar to most programmers and are only supported by a few languages.

One of the languages supporting exceptions is Eiffel. In Eiffel, exceptions are closely linked with the notion of contracts. Abnormal events occur whenever a contract is violated, that is when either a pre-condition, post-condition or invariant is not satisfied. For each of these cases an exception handler may be specified in the class defining the object for which an exception is raised. The way in which abnormal events are dealt with in Eiffel conforms to what is called the resumption model in  [BuhrMac92]. The resumption model allows a handler invoked for an exception to resume the computation after correcting the faulty situation. In Eiffel, the retry statement may be used to try the operation that resulted in raising an exception again.

Exceptions are part of the language definition of C++, but not many of the available compilers provide support for exceptions. The declaration of exceptions conforms to the definition of classes. For example, slide 6-abnormal declares a Matherr and an Overflow exception, which is publicly derived from Matherr. Obviously, the advantage of employing the class mechanism is that inheritance may be used to organize exceptions in groups. The most specific exception may then be tested for first, as in the catch statements following the try block in slide 6-abnormal. As the example shows, exception handling is activated by wrapping a group of statements or function calls in a try block. Whenever a failure occurs, the exception handlers declared by the subsequent catch statements will be tried to do something about it.

An exception may be raised by means of a throw statement as illustrated for the Assert function. The Assert function mimics the behavior of the assert macro, but in addition allows us to raise an explicit exception such as BadArgs or Invariant (provided these are defined as exceptions).

In contrast to Eiffel, which allows for resuming the computation after the occurrence of an exception, C++ supports only termination semantics for exceptions, which results in terminating the function call that caused the exception. The main reason to adopt termination semantics is (according to  [St94]) that resumption semantics invites programmers to employ exceptions for things other than that for which they are meant, such as debugging and the like, or allocating new resources. However, such tasks are often better performed by an explicit handler function as may, for example, be defined to handle allocation problems when calling the new operator. Exceptions, on the other hand, are primarily meant to be used to handle errors for which no such remedies exist.

Lacking adequate language support, both exceptions and interventions may be simulated, respectively by setting a global flag which is tested after the operation and by adding intervention routine parameters to function definitions. However, such simulations are tedious and error-prone to implement and, moreover, violate encapsulation and endanger extensibility.