Section 3 - Throwing Exceptions and Nested Classes
5B.3.1 Creating Classes With Custom Exceptions
Our Rational number class of a previous lesson was incomplete. Besides not having overloaded the insertion operator (we had not learned how at that point), we silently handled zero denominators without informing our client. It is very hard to tell the client about a problem from inside a constructor. Constructors do not return values, so the only way to inform the client is through static class members designed to flag errors (bizarre, but possible) and the preferred exception mechanism.
In order to prepare to throw exceptions, we first decide how many and what kind of exceptions we want our class methods to throw. We give each exception a name, then create an empty nested class with that name inside our class of interest. For the Rational class, I'd like to have two different types of exceptions. Because the old Rational class only had one kind of error - zero denominator - I'm going to add a second constructor that takes a string. That string would ideally be a rational number, like "1/2", "543/-1002", "12" or "-999". However, if the user tried to pass "23z/5" or "4.9" then we should generate an exception different than a zero denominator exception. So we create two exception nested classes:
- ZeroDenominator - signifies a constructor or mutator (set()) attempting to pass a 0 denominator
- BadRationalString - signifies a string that has illegal characters or a badly formed fraction
This is how we would prototype a class like Rational with these exception classes contained therein:
// Rational prototype -------------------------------- class Rational { // friend operators friend Rational operator+(Rational r1, Rational r2); friend Rational operator-(Rational r1, Rational r2); friend ostream & operator<<(ostream& strm, Rational& r); private: long num, den; long gcd(); void reduce(); public: Rational( long n = 0, long d = 1 ); Rational( string ratString ); void set( long n = 0, long d = 1 ); void set( string ratString ); void show(); // exception classes class BadRationalString { }; class ZeroDenominator { }; };
This prototype promises many improvements over our previous rendition:
- There are mutators, set(), that can be used to change the values of an existing Rational object.
- There is a string version of both set() and the constructor, to allow initialization by strings.
- There is an overloaded insertion operator.
- There are nested classes that we can use to distinguish two kinds of exceptions.
Aside on Nested ClassesNote that these are not sub-classes.They are nested classes. You can nest any classes in C++, not just exception classes. If you nest a class that contains real data, then you would place the prototype inside the class prototype and then define the methods using the double scoping syntax:
OuterClass::NestedClass::method() { // nested class method definition }OuterClass::NestedClass::method() { // nested class method definition }
5B.3.2 Throwing Your Own Exceptions
Look at the set() method definition. It demonstrates how we throw an exception:
void Rational::set(long n, long d) { // defaults in case of user error (bad d) num = 0 ; den = 1; if (d==0) throw ZeroDenominator(); num = n; den = d; reduce(); }
Very simply, if the parameter passed into the denominator is 0, we throw a ZeroDenominator exception. That is like an immediate return, except it has the added value that it tells the client a serious problem occurred.
The second set() overload is much trickier logically because it involves parsing a string to convert it to two ints. However, the exception throwing lines are equally simple. The difference is that we throw a BadRationalString exception.
The style of this long method is worth your time. It demonstrates good programming practice. Every section is commented so you know its purpose. Temporary variables are used rather than long hard-to-read lines that try to do everything at once. It makes use of the sibling set() once the strings are converted to ints, rather than duplicating the code. Here it is. I hope you go through it line-by-line and understand everything. If not, you can ask me.
#include <sstream> // need to allow for istringstream() void Rational::set(string ratString) { long tryNum, tryDen, slashPos, lengthOfDenStr; string strNum, strDen; int k; // contingency values in case we throw exception num = 0; den = 1; // must be of form "int/int" slashPos = ratString.find("/"); if (slashPos == 0 || slashPos > 50) throw BadRationalString(); // returns immediately if ( slashPos < 0 ) { // no "/". hopefully just an int strNum = ratString; strDen = "1"; } else { strNum = ratString.substr(0, slashPos); lengthOfDenStr = ratString.length() - slashPos - 1; strDen = ratString.substr(slashPos+1, lengthOfDenStr); } // make sure strNum is valid number // first digit can be digit, or +/- if ( !isdigit(strNum[0]) && strNum[0] != '-' && strNum[0] != '+') throw BadRationalString(); // returns immed. // rest must be digits for (k = 1; k < strNum.length(); k++) if ( !isdigit(strNum[k]) ) throw BadRationalString(); // returns immed. // same test for strDen if ( !isdigit(strDen[0]) && strDen[0] != '-' && strDen[0] != '+' ) throw BadRationalString(); // returns immed. for (k = 1; k < strDen.length(); k++) if ( !isdigit(strDen[k]) ) throw BadRationalString(); // returns immed. // convert to ints istringstream(strDen) >> tryDen; istringstream(strNum) >> tryNum; set(tryNum, tryDen); }