X3J16/94-0021 WG21/N0408 A Proposed Exception Class Hierarchy Revision 2 Thomas Keffer Rogue Wave Software, Inc. Technical Report 94-01 P.O. Box 2328 Corvallis, OR 97339 (503) 754-3010 (c) Copyright Rogue Wave Software, Inc. 1993-1994 1.0 Revision history Revision 2.0 This revision reflects input from Dag Bruck, Ron Guilmette, Mats Henricson, Andrew Koenig, Nathan Myers, Rob Savoye, Jerry Schwarz, Bjarne Stroustrup, Mike Stump, and Mike Vilot. The overall feedback that I got was to simplify, simplify! Reflecting this, ... As a whole, the classes are now much simpler. Now uses class "string" throughout, rather than NTBSs. The results of using string in a xalloc handler (i.e., calling member function what()), is now left implementation defined. Class xmsg now carries only "what", not "where" and "why". Implementation of any contained strings and their ownership is now left unspecified. No longer uses a "raise handler". No longer specifies that exceptions should be thrown using raise(). Revision 1.0 First Revision 2.0 Introduction and rationale As the use of components grows, a programmer will be faced by a bewildering variety of possible exceptions that might be thrown at him from low- level classes. This proposal is an attempt to put some order in the situation. Fortunately, the C++ exception architecture allows exceptions to be thrown and caught according to their type, allowing hierarchies of throw types. This paper builds on material from a variety of sources, particulary Jerry Schwarz's original "Standard Exceptions" paper (X3J16/91-0116 WG21/0049) andPJ Plauger's Library draft (X3J16/93- 108 WG21/N0315) and addendum (X3J16/93-0110 WG21/N0317). 2.1 Design goals The following are fundamental assumptions that go into this paper: 1.The exception hierarchy should reflect the decision making process that a programmer might make in deciding how to handle an exception. That is, early branches in the inheritance DAG should reflect very different strategies in how an exception should be handled. This strategy should be contrasted with one which groups type of exceptions by, say, the facility that threw them. 2.It should always be possible to program in a style that guarantees that no exceptions will occur. This is to support embedded and other systems that cannot afford the space or time costs of exceptions. Presumably, it would be desirable for an implementation to offer command line switches for such a programmer that suppress the building of call frames to support exceptions. 3.In low memory situations it should be possible to guarantee that allocations off the heap are not made. Naturally, this requires that out-of- memory exceptions not use the heap. 2.2 An error model This section discusses an error model, to guide us in designing an exception hierarchy. In the proposed model, errors are divided into two broad categories: logic errors and runtime errors. The distinguishing characteristic of logic errors is that they are due to errors in the internal logic of the program. In theory, they are preventable. As you might expect, they can be difficult to recover from and, indeed, the default response is commonly to abort the program. By contrast, runtime errors are due to events beyond the scope of the program. They cannot be easily predicted in advance. 2.2.1 Logic errors Logic errors are due to faulty logic or coding in the program. A large subclass of such errors (but not all) are violated preconditions or domain errors. Another large class is violated class invariants. Examples of logic errors are: Out of bounds errors; Attempting to use a bad file descriptor; Calling acos with an argument outside of the domain -1.0 to 1.0. In theory, given enough diligence on the part of the programmer, these kinds of errors are preventable. In practice, of course, they will occur. The treatment of logic errors can be divided into two general categories, depending on the cost of error detection and, hence, whether or not the system can afford to detect the error: non- recoverable logic errors and recoverable logic errors. 2.2.1.1 Non-recoverable logic errors The performance characteristics of some code may be so stringent that the system cannot afford to check for violated preconditions. An example is indexing or bounds errors: the cost of checking that an index is in bounds may well exceed the cost of the array access itself. Hence, for good performance, it may be necessary to forgo error checking. However, as a debugging aid, it is useful to offer the programmer "debug" versions of the libraries which, although slower and bulkier that the production version of the library, do check for such errors. 2.2.1.2 Recoverable logic errors If the cost of error detection is relatively low, then it starts to make sense to detect an error in the production version of the library. An example is a bounds error in a linked list: the cost of walking the list will far exceed the cost of detecting whether the index is in bounds. Hence, you can afford to check for a bounds error on every access. 2.2.1.3 Recoverable or not? We have pointed out that, depending on the cost of error detection, a library may elect to forgo checking preconditions. But there is another kind of cost and that is the cost of the consequences of an undetected error. Unfortunately, this cost cannot be known by the sort of low-level classes found in the Standard C++ Library. This suggests that the library should assume the worse and check for all possible errors. Indeed, this has been the approach of all proposed classes and so all possible logic errors are treated as potentially recoverable. Alternatively, the interface could be augmented with additional functions that do not do any precondition (domain) checking. 2.2.2 Runtime errors The distinguishing characteristic of runtime errors is that they cannot be reasonably predicted in advance without extraordinary efforts on the part of the programmer. They are frequently caused by either external conditions beyond the scope of the program, or by range errors (violated postconditions) such as arithmetic overflow from legal arguments. Examples of runtime errors are: Attempt to set a date object to a bad date (E.g., "31 June 1993"); Attempt to invert a singular matrix; Disk full error; Out of memory. 2.2.3 Logic or runtime? The line between logic and runtime errors can sometimes be fuzzy. For example, the constructor for a date class could state a precondition: "don't give me an invalid date" and then the programmer would be responsible for detecting a bad date before invoking the constructor. To do otherwise would be a logic error. Of course, this is a lot of work and, in any case, the date object is probably in a better position to detect the bad date than its client. Hence, a better choice would be to treat such an error as a runtime error. Very frequently a runtime error can be converted into a logic error. An example is IEEE arithmetic: overflow is accounted for in the postcondition. It's not actually an error until the results are fed into another function at which point it becomes a violated precondition. Another example is the Standard C++ Library string class. It could be designed such that length errors are eliminated: it would not be an error to augment (append) a string beyond it's maximal theoretical capacity (NPOS less one), only to use such a string. Finally, it is worth noting that while a good error model suggests a good exception class hierarchy, the two issues are not necessarily coupled. Having identified an error model, one could as easily design an error handling strategy around return values rather than exceptions. 2.3 Hierarchy The following, singly rooted, hierarchy is proposed: exception logic badcast domain runtime range alloc The following sections discuss the classes in more detail. o Including classes domain and range may provide finer detail than is necessary in a standard exception hierarchy. However, all of the classes proposed for the Standard C++ Library seem to specify one or the other or both. For example, the standard string class specifies class outofrange The exception class string::outofrange should most properly be named string::outofdomain., (a possible subclass of domain) and class lengtherror (subclass of range). 2.4 Header These classes shall be declared in the header . 2.5 Name space The usual namespace convention shall be followed. 3 Class exception class exception { public: exception(const string&); exception(const exception&); exception& operator=(const exception&); virtual ~exception(); virtual string what() const; protected: exception(); protected: const string* what_; // For exposition only bool doFree_; // For exposition only }; The class exception is the root class for all types that can be thrown as exceptions by the Standard C++ Library. o Note that the specification for a function may choose to treat an error by some other means than throwing an exception (indeed, the Standard C Library uniformly does not throw exceptions). However, if it does choose to thrown an exception, then the throw type should inherit from this class. This same comment applies to subclasses of exception. o The overall approach is that the results of calling what() on an object of type alloc is implementation defined. It may work, it may not. 3.1 Constructors 3.1.1 exception(const string& what); Constructs an instance of class exception and stores a copy of the string what internally. o Conceptually, this can be done by initializing what_ to point to a copy of what, allocated off the free store. Ownership is denoted by setting doFree_ to true. 3.1.2 exception(); This protected default constructor shall do no allocations off the free store. The results of calling what() on any object constructed in this manner is implementation defined. o Conceptually, this can be accomplished by setting what_ to nil, and doFree_ to false. Note that this constructor is used by alloc. 3.1.3 exception(const exception& x); Copy constructor. Constructs an instance of class exception that is a copy of x. o This can be accomplished as follows. If doFree_ is true, then set what_ to point to a copy of the string pointed to by x.what_. Otherwise, set what_ to x.what_. 3.2 Assignment 3.2.1 exception& operator=(const exception& x); If the object pointed to by this is identical to the object x, then the assignment operator does nothing. Otherwise, it releases any resource allocated to *this then copies the value of x into *this. Returns *this. 3.3 Destructor 3.3.1 virtual ~exception(); Destroys an object of class exception and releases any resources allocated by it. o Hence, if the exception was constructed by the protected constructor, then it allocated no resources and is under no obligation to release any. 3.4 Functions 3.4.1 virtual string what() const; If the instance of exception was constructed using the public constructor, then returns a copy of the internally held string. Otherwise, the results are implementation defined. o Hence, the results of calling what() on an object of type alloc is implementation defined. 4 class logic class logic : public exception { public: logic(const string& what); }; The class logic is the base class for objects thrown as exceptions by functions in the Standard C++ Library in response to logic errors. This includes, but is not limited to, violated preconditions or violated class invariants. o Again, the wording is intended such that the specification for a function may allow some other means of treating a logic error. However, if the specification says to throw an exception, then the throw type should inherit from this class. 4.1 Constructor 4.1.1 logic(const string& what); Constructs an instance of class logic and passes its argument to the public constructor of exception. 5 class badcast class badcast : public logic { public: badcast(const string& what); }; The class badcast is the base class for objects thrown as exceptions to report the execution of an invalid dynamic-cast expression. 5.1 Constructor 5.1.1 badcast(const string& what); Constructs an instance of class badcast and passes its argument to the public constructor of logic. 6 class domain class domain : public logic { public: domain(const string& what); }; The class domain is the base class for objects thrown as exceptions by functions in the Standard C++ Library in response to domain errors. 6.1 Constructor 6.1.1 domain(const string& what); Constructs an instance of class domain and passes its argument to the public constructor of exception. 7 class runtime class runtime : public exception { public: runtime(const string& what); protected: runtime(); }; The class runtime is the base class for objects thrown as exceptions by functions in the Standard C++ Library in response to runtime errors. This includes, but is not limited to, out of memory, out of range, environmental errors, etc. 7.1 Constructors 7.1.1 runtime(const string& what); Constructs an instance of class runtime and passes its argument to the public constructor of exception. 7.1.2 runtime(); This protected default constructor shall do no allocations off the free store. The results of calling what() on any object constructed in this manner is implementation defined. This constructor is used by alloc to ensure that no memory allocations happen. 8 class range class range : public runtime { public: range(const string& what); }; The class range is the base class for objects thrown as exceptions by functions in the Standard C++ Library in response to range errors. 8.1 Constructor 8.1.1 range(const string& what); Constructs an instance of class range and passes its argument to the public constructor of runtime. 9 class alloc class alloc : public runtime { public: alloc(); private: static string allocmsg; // For exposition only }; The class alloc is the base class for objects thrown as exceptions by functions in the Standard C++ Library in response to an inability to allocate storage. 9.1 Constructor 9.1.1 alloc(); This constructor shall do no allocations off the free store. The results of calling what() on any object constructed in this manner is implementation defined. 10 Specializations In the present WP, various classes have nested classes that are thrown in response to an exceptional condition. For example, class string has classes outofrange and lengtherror, thrown in response to an out of bounds and range error, respectively. These classes should inherit from the appropriate class outlined in this proposal. For example, class string { public: class outofdomain : public exception::domain { public: outofdomain(const string&); }; class lengtherror : public exception::range { public: lengtherror(const string&); }; /* ... */ }; Note that class string::outofrange has been renamed string::outofdomain. This is because an out of bounds error is most properly a domain error, not a range error. ------ Thomas Keffer | Internet: keffer@roguewave.com Rogue Wave Software, Inc. | uucp: ...!orstcs!rwave!keffer PO Box 2328 | CompuServe: 70744,2604 Corvallis, OR 97339 | BIX: tkeffer 503-754-3010 (voice) 503-757-6650 (FAX)