Previous Table of Contents Next


I concluded that the ability to define groups of exceptions is essential. For example, a user must be able to catch any I/O library exception without knowing exactly which exceptions those are. Many people, including Ted Goldstein and Peter Deutsch, noticed that most such groups were equivalent to class hierarchies. We therefore adopted a scheme inspired by ML where you throw an object and catch it by a handler declared to accept objects of that type. This scheme naturally provides for type-safe transmission of arbitrary amounts of information from a throw point to a handler. Here’s an example:

   class Matherr { /* ... */ };
   class Overflow : public Matherr { /* ... */ };
   class Underflow : public Matherr { /* ... */ };
   class Zerodivide : public Matherr { /* ... */ };
   class Int_add_overflow : public Overflow { /* ... */ };

   void f()
   {
       // ...

       try {
           f();
       }
       catch (Overflow& over) {
           // handle Overflow or anything derived from Overflow
       }
       catch (Matherr& math) {
           // handle any Matherr
       }

       // ...
   }

Thus f() might be written like this:

   void f() throw(Matherr&) // f() can throw Matherr&
                             // exceptions (and only Matherr& exceptions)
   {
       // ...
       if (d == 0) throw Zerodivide();
       // ...
       if (check(x,y)) throw Int_add_overflow(x,y);
       // ...
   }

Zerodivide is caught by the Matherr& handler, and Int_add_overflow is caught by the Overflow& handler that might access the operand values x and y passed by f() in the object thrown.

The central point in the exception-handling design was the management of resources. In particular, if a function grabs a resource, how can the language help the user to ensure that the resource is correctly released upon exit even if an exception occurs? The problem with most solutions are that they are verbose, tedious, potentially expensive, and therefore error prone. However, I noticed that many resources are released in the reverse order of their acquisition. This strongly resembles the behavior of local objects created by constructors and destroyed by destructors (section 6.2.4.8). Thus we can handle such resource acquisition and release problems by a suitable use of objects of classes with constructors and destructors. Here’s an example:

   void f(const char* file_name, Lock& lck)
   {
       File_ptr p(file_name,”r”);    // open file and use through “p”
       Lock_ref r(lck);              // acquire lck

       // code using p relying on lck being held
   }

The file is implicitly closed by p’s destructor and the lock is implicitly released by r’s destructor.

I named this technique “resource acquisition is initialization.” It provides a systematic and declarative way of dealing with cleanup problems that have traditionally been addressed by ad hoc techniques relying on explicitly catching every exception to gain control at the return from a function (called catch(...) in C++ and finally in some other languages). The ad hoc techniques are error prone in that they rely on programmers consistently adding code to deal with error conditions—and avoiding such reliance is one of the main reasons to use exceptions. The “resource acquisition is initialization” technique extends to partially constructed objects and thus addresses the otherwise difficult issue of what to do when an error is encountered in a constructor.

During the design, the most contentious issue turned out to be whether the exception-handling mechanism should support termination semantics or resumption semantics—that is, whether it should be possible for an exception handler to require execution to resume from the point where the exception was thrown. The main resumption-versus-termination debate took place in the ANSI C++ committee. After a discussion that lasted for about a year, the exception-handling proposal as presented in the ARM (that is, with termination semantics) was voted into C++ by an overwhelming majority. The key to that consensus was presentations of experience data based on decades of use of systems that supported both resumption and termination semantics by representatives of DEC, Sun, Texas Instruments, IBM, and others. Basically, every use of resumption had represented a failure to keep separate levels of abstraction disjointed.

The C++ exception-handling mechanism is explicitly not for handling asynchronous events directly. This view precludes the direct use of exceptions to represent something like hitting a Del key and the replacement of UNIX signals with exceptions. In such cases, a low-level interrupt routine must somehow do its minimal job and possibly map into something that could trigger an exception at a well-defined point in a program’s execution.

As ever, efficiency was a major concern. The C++ exception-handling mechanism can be implemented without any runtime overhead to a program that doesn’t throw an exception (Stroustrup, 1988a). It is also possible to limit space overhead, but it is hard simultaneously to avoid runtime overhead and code size increases.

6.5.7. Namespaces

C++ followed C in having a single global namespace. This caused name clashes in larger programs and left the class as C++’s only language mechanism for directly supporting grouping of declarations. However, classes are designed to support the declaration and use of objects, and the support for objects gets in the way of (mis)using classes as modules. I regret not providing C++ with some form of namespace control early on when that could have been done without debate and when experimentation was easy. However, the resolution mechanisms I looked at, such as Ada packages and Modula-2 modules, had too great an overlap with the C++ class mechanism and thus didn’t fit. Furthermore, separate compilation alleviated the problem—and thus postponed its proper solution.


Previous Table of Contents Next