Monday, December 21, 2009

Syntax of C++ Exceptions




I l@ve RuBoard


Syntax of C++ Exceptions


C++ exceptions allow the programmer to change the flow of control when some event occurs, for example, an error. These errors happen at run time (file is not found, index is invalid, etc.); when C++ raises an exception, the program may terminate if it has no handler that knows how to deal with that exception.



Exception handlers are segments of program source code that should be executed when the exception is raised, for example, printing a message to the user, collecting information for the analysis of the causes of the exception, or error recovery.



Organizing error-related source code into exception handlers can make flow of control more logical; instead of doing all the tests in the mainline of the algorithm and thus obscuring its meaning, error handling is coded in a separate place. The tradeoff of this approach is the possibility of obscuring the meaning of server functions that are involved in error discovery.



This separate place for error recovery can be in the same method that caused the exception, in a caller of that method, in a caller of the caller, and so on. This flexibility makes the design with exceptions more difficult. However, the exception mechanism allows the programmer to transfer control to recovery actions in a disciplined way.



Presumably, C++ exceptions allow the programmer to isolate exceptional cases in other, remote segments of source code and streamline the base processing. It should make the program more readable so that it is easier to understand. I am not sure that this is often the case. As I mentioned earlier, the utility of using exceptions is in eliminating extra parameters, return values, and complex calling conventions among the functions that discover the problem and the functions that try to recover from the problem.



When C++ raises or "throws" an exception, it can create an object of a predefined class Exception or of a programmer-defined class. This programmer-defined class can be derived from class Exception, or it can be an independent class. Again, this flexibility makes designing with exceptions more open ended and hence more difficult to understand.



The exception might be thrown explicitly with the throw statement or implicitly as the result of an illegal or invalid action. The exception is caught with the catch statement, and control is transferred to the statement that caught the exception. The catch statement (or block of statements) performs the error recovery (if any). The return of control after the error recovery in the catch block depends on the structure of the program. In general, it does not return by itself to the place where the exception occurred. This is why the programmer has to structure the program in a specific way if such return (e.g., to continue processing) is desirable.



There are three operations related to exception handling:





  • throwing an exception





  • catching an exception





  • claiming an exception






Throwing an exception means specifying that certain exceptional (possibly erroneous) conditions are discovered and that they should be processed using the C++ exception mechanism rather than common control flow techniques.




Catching an exception means specifying a segment of code that is designed to respond to a particular exceptional condition but not to other conditions.




Claiming an exception means specifying what exception can be raised within this method; it helps the compiler (and the client programmer and the maintenance programmer) know what to expect from the function and how it should be used.




Throwing an Exception


To raise an exception, the keyword throw is used. Its usage indicates that the server code has discovered a condition that it does not know how to handle, and it throws the exception in the hope that someplace (among its client or its client's clients) there will be a segment of code that knows how to handle the situation.



The keyword throw is used in a throw statement. Its general syntactic form includes the keyword throw with an operand that can be a value of any type to be thrown in search of the exception handler.





throw value;



The throw statement is usually executed conditionally after testing some values or relationships in the program and discovering that they do not satisfy requirements. This means that the server code executes the throw statement to notify the client of the problem discovered in the server code.



The throw statement can take only a single operand of any type. However, some compilers do not flag the throw statement as an error if you try to throw more than one value. The value of the throw operand is used by the client code (that tries to process the exception) to retrieve information about the context of the error. Often, this information is used to define the client code behavior in error recovery.



Here is a modified example of the function inverse(). In Listing 18.3, this function sets up the return values or parameter values to communicate with the client code. In this version, the function inverse() throws exceptions in two cases: (1) if it discovers that the denominator is zero and (2) if it discovers that the denominator is negative.





inline void inverse(long value,double& answer) // two parameters
{ answer = (value) ? 1.0/value : DBL_MAX;
if (answer==DBL_MAX)
throw MSG::msg(1); // zero denominator
if (value < 0)
throw value; } // negative denominator



You see that in the case of a zero denominator, the function throws a value of the character array type, and in the case of a negative denominator, it throws a value of the long type. The fact that these types are different is no accident. It would be more difficult to handle the exception handling if both throw statements threw the values of the same type. If both exceptions have to be processed the same way, this is not a problem. If the exceptions have to be processed differently, the client code would have to figure out what really occurred in the server code that threw the exception.



If you compare this function inverse() with its version in Listing 18.1, you will see that their interfaces are similar: Both functions return a void type and have only two parameters. In Listing 18.1, the function inverse() did not try to discover any exceptions. Neither did its client fraction(). It was the job of the main() client code to discover both exceptions (zero denominator, negative denominator) and process them.



In Listing 18.2 and Listing 18.3, functions inverse() and fractions() were trying to discover exceptions, recover from some of them (zero denominator) and let the main() client recover from others (negative denominator). The result was greater coupling and more confusing code. The last version of inverse() throws both exceptions. It does have some analysis code (to decide what exception to throw, if any), but its interface is as simple as in the first version in Listing 18.1. This simplicity will be paid for by the additional code I will write to catch and to claim these exceptions.





Catching an Exception


The function inverse(), which can throw two exceptions, has a direct client: function fraction() that calls inverse() and an indirect client: function main() that calls fraction(). In general, the hierarchy of calls can be arbitrarily high. If a function, in this example inverse(), throws an exception and does not process this exception itself, one of its callers (direct or indirect) has to catch this exception.




Catching an exception is a process of finding the code that can handle the error (the exception handler); this is done by searching through a chain of function calls.



One might think that catching an exception requires the keyword catch. This is true: C++ does have the keyword catch, and this keyword is used in catching the exception. But this is not enough. When a function catches exceptions, it cannot catch them from an arbitrary source of exceptions. The function has to indicate from what segment of its code it will try to catch exceptions. This requires the use of yet another C++ keyword: try. This keyword should be followed by a block that can throw exceptions.



The client code that has the responsibility of catching errors encloses the code that can raise the exception in a try statement.





void foo() // function that catches exceptions
{ try // the try statement
{ statements; } // statements that throw exceptions
厎 // the rest of foo() with catch blocks



C++ exception handlers are implemented using the keywords try and catch; the statements (or method calls) that may throw exceptions are put in the try blocks; the exception handlers themselves are put into the catch blocks.



Syntactically, one or more catch blocks should follow a try block. Each catch block has a parameter of the type that corresponds to the exception that this catch block handles.





void foo() // function that catches exceptions
{ try
{ statements; } // statements that throw exceptions
catch (Type1 t1) // catch block for thrown type Type1
{ handler_for_Type1(); }
catch (Type2 t2) // catch block for thrown type Type2
{ handler_for_Type2(); }
. . . . .
catch (TypeN tN) // catch block for thrown type TypeN
{ handler_for_TypeN(); }
statements_executed_after_the_try_or_catch_block; }



The try statement must be followed by at least one catch construct (block), which provides exception handling. It is an error to use a catch block that is not preceded by a try statement. (It is all right if there are other catch blocks between this one and the preceding try statement.) It is an error to use a try statement that is not followed by a catch block or blocks.



Recall that the throw statement has an argument of some type梐 character array, a long value, or even a value of some programmer-defined class type. The value of the argument usually carries some data about the context of the error. In the case of the inverse() function, this data is either a string with the message to be printed or the negative denominator value to be displayed. If the throw statement throws an object of a class type, the constructor for that type should enable the object to carry some information about the problem. This information might be used by the catch construct for diagnostics and error recovery.



If there are several catch constructs after the try block, they have to have arguments of different types. Since catch constructs do not have names, the signatures of these constructs must be unique.



If the exception type thrown in the try block "matches" the argument of a catch construct, the code in the catch construct is executed and the search stops; when the catch block terminates, the statements that follow the catch blocks for this try statement are executed




"Matching the argument" means that the exception object that is thrown by the try block can be assigned to the parameter of the catch block, meaning the exact match, any of the standard conversions, or any of subclasses of the parameter of the catch construct. For example, a double value can be caught by a catch block with a long parameter, and a SavingsAccount object can be caught by a catch block with an Account parameter.



After the catch block terminates, the statements that follow the try block and its catch constructs are executed. These statements can have other try blocks (followed by catch constructs) if necessary. If the try statement did not throw any exception, the catch constructs are treated as the null statements梩hey are skipped.



If the exception was thrown in the middle of the try statement, the execution of the try statement is terminated, the catch construct is found and executed, and so on; the statements in the try block that follow the one that threw the exception are never executed. Usually, this is only logical: The exception was thrown because these statements could not be executed.



What happens if the code in the try block throws an exception that does not have a catch construct of the appropriate type? Too bad梩he function is terminated: Not only is the try block not executed in its entirety, but the code that follows the catch constructs is not executed either. This means that the appropriate catch block will be searched for in the client code of the function. If it is found, all is fine and well. If the catch construct capable of handling the exception is not found even in main(), the program is terminated.



Let us consider, for example, the following version of the inverse() function that throws the exceptions and tries to catch them.





inline void inverse(long value,double&answer) // two parameters
{ try // start of try block
{ if (value == 0) // zero denominator
throw MSG::msg(1);
if (value < 0) // negative denominator
throw value;
answer = 1.0 / value; } // end of the try block
catch (char* str)
{ cout << str; } // zero denominator
catch (long val)
{ cout << MSG::msg(2) << val << "\n\n"; }} // negative value



If the first argument has a legitimate value, the try block is executed completely, and the catch blocks are skipped. There are no statements following the catch blocks, so the function terminates as if it had no exception handling at all.



If the first argument is zero, the character array exception is thrown and the first catch block is executed. Notice that the catch block is a "block"梚t has its own scope, and it refers to its parameter str rather than to the variable that has actually been thrown: MSG::msg(1).



Similarly, if the first argument is negative, its value is thrown, and the second catch block is executed. Again, the name of the value to be printed is val rather than value. No matter what exception is thrown, the statement answer = 1.0/value is never executed. This is reasonable because this statement should be executed only if the value passes all the tests.



If the statements in the try block throw an exception that does not have a catch block to handle it in the function inverse(), the search for the exception handler continues in fraction() and then in main().



In this version of the inverse() function, the throw statements and the catch blocks are in the same function scope. Syntactically, this is legal C++. From the software engineering point of view, this is overkill梩here is no need to use the exception handling mechanism if the information about exception is not passed across different functions. In this case, a simple if statement within inverse() would give the same results.



Another problem with this version of exception handling is related to the execution of the rest of the program. After the function inverse() terminates, its callers, fraction() and main(), have no idea whether any exceptions were raised. Meanwhile, if any exception was raised, the statement that computes the answer was not executed, and callers of inverse() should know about that.



Next, let us again consider the version of inverse() that throws exceptions but does not catch them.





inline void inverse(long value,double& answer) // two parameters
{ answer = (value) ? 1.0/value : DBL_MAX;
if (answer==DBL_MAX)
throw MSG::msg(1); // zero denominator
if (value < 0)
throw value; } // negative denominator



Let us try to catch these exceptions in the client function fraction().





inline void fraction (long numer, long denom, double& result)
{ try {
inverse(denom, result); // result = 1.0 / denom
result = numer * result; } // result = numer / denom
catch (char* str)
{ cout << str; } // zero denominator
catch (long val)
{ cout << MSG::msg(2) << val << "\n\n"; }} // negative value



This is not better than the previous version of inverse(). The exceptions should be processed in such places in the client code where the information about the exception can be used to change the behavior of the program, in this case, skipping the display of the result of computations.




Listing 18.4 demonstrates this example with exception handling in the main() function. As I noted earlier, this example is somewhat artificial because main() could discover that the input is invalid itself. Assuming it cannot do that, this scheme of exception handling makes sense: inverse() discovers the error and sends information to main() so that main() could skip the use of invalid results.



In Listing 18.4, function inverse() analyzes the situation and throws two expressions for the benefit of its callers. Its immediate caller, fraction(), does not have any exception handlers (catch constructs) because it is in the function main() where the statement to be skipped is located. Since fraction does not have any catch constructs, it does not have a try statement either because it would be illegal to have a try statement without catch constructs.



If inverse() does not throw exceptions, fraction() and main() continue to compute and to print the result and to request the next set of data. If inverse() throws an exception, it is not processed in inverse() because it does not have appropriate catch constructs. The search propagates to fraction(). Since fraction() does not have any exception handlers, the search propagates to main(). If main() does not have any exception handlers either, the program terminates.



When the search percolates to main(), it finds there both the try statement and the catch constructs. From the point of view of main(), it is its server function fraction() that is the source of trouble. The client main() does not care whether fraction() received the exception from one of its servers or threw it itself. If fraction() throws an exception, the execution of the try block is terminated before the answer is displayed. The corresponding exception handler prints a message that uses the information generated in inverse().





Example 18.4. Example of throwing and catching exceptions.


#include <iostream>
#include <cfloat>
using namespace std;

class MSG {
static char* data []; // internal static data
public:
static char* msg(int n) // public static method
{ if (n<1 || n > 5) // check index validity
return data[0];
else
return data[n]; } // return valid string
} ;

char* MSG::data [] = { "\nBad argument to msg()\n",
"\nZero denominator is not allowed\n\n", // depository of text
"\nNegative denominator: ",
"Enter numerator and positive\n",
"denominator (any letter to quit): ",
"Value of the fraction: "
} ;

inline void inverse(long value, double& answer)
{ answer = (value) ? 1.0/value : DBL_MAX;
if (answer==DBL_MAX)
throw MSG::msg(1);
if (value < 0)
throw value; }

inline void fraction (long numer, long denom, double& result)
{ inverse(denom, result); // result = 1.0 / denom
result = numer * result; } // result = numer/denom

int main()
{
while (true)
{ long numer, denom; double ans; // numerator/denominator
cout << MSG::msg(3) << MSG::msg(4); // prompt user for data
if ((cin >> numer >> denom) == 0) break; // enter data
try {
fraction(numer,denom,ans); // compute answer
cout << MSG::msg(5) << ans <<"\n\n"; // valid answer
}
catch (char* str) // zero denominator
{ cout << str; }
catch (long val) // negative value
{ cout << MSG::msg(2) << val << "\n\n"; }
}
return 0;
}


How large should a try block be? In this example, the try is composed of two statements: a call to fraction() and the output statement. What happens if I move the call to fraction() outside of the try block?





int main()
{ while (true)
{ long numer, denom; double ans; // numerator/denominator
cout << MSG::msg(3) << MSG::msg(4); // prompt user for data
if ((cin >> numer >> denom) == 0) break; // enter data
fraction(numer,denom,ans); // compute answer
try {
cout << MSG::msg(5) << ans <<"\n\n"; } // valid answer
catch (char* str) // zero denominator
{ cout << str; }
catch (long val) // negative value
{ cout << MSG::msg(2) << val << "\n\n"; } } // end of loop
return 0; }



This design misses the boat. The try statement will not raise any exceptions. And the catch blocks will never intercept any梩hey can process only exceptions that originate within the preceding try statement. When inverse() throws an exception to fraction(), and fraction() throws this exception to main(), no catch block will handle the exception, and the program terminates.



What about putting only the function call in the try block, leaving the output statement outside? The rationale for doing that is that since this statement does not throw any exceptions, it pollutes the precious space in the try block.





int main()
{ while (true)
{ long numer, denom; double ans; // numerator/denominator
cout << MSG::msg(3) << MSG::msg(4); // prompt user for data
if ((cin >> numer >> denom) == 0) break; // enter data
try {
fraction(numer,denom,ans); } // compute answer
cout << MSG::msg(5) << ans <<"\n\n"; // valid answer
catch (char* str) // zero denominator
{ cout << str; }
catch (long val) // negative value
{ cout << MSG::msg(2) << val << "\n\n"; } } // end of loop
return 0; }



This results in a syntax error. The output statement is now between the try statement and the catch blocks. Hence, the try statement is not directly followed by the catch constructs. To add insult to injury, the catch blocks are not immediately preceded by the try statement. What exactly the compiler will tell you is anybody's guess, but you are not going to like it.



What about expanding the try statement, including in it more loop statements? The rationale for that might be to combine different sources of exceptions and process them in one cache of catch constructs.





int main()
{ while (true)
{ long numer, denom; double ans; // numerator/denominator
try {
cout << MSG::msg(3) << MSG::msg(4); // prompt user for data
if ((cin >> numer >> denom) == 0) break; // enter data
fraction(numer,denom,ans); // compute answer
cout << MSG::msg(5) << ans <<"\n\n"; } // end of try
catch (char* str) // zero denominator
{ cout << str; }
catch (long val) // negative value
{ cout << MSG::msg(2) << val << "\n\n"; } } // end of loop
return 0; }



This is doable and would be useful if this segment of client code produced additional exceptions. In general, however, it is desirable to keep the scope of the try statement as narrow as possible to make it easier for the maintenance programmer to figure out where the exceptions could come from.



And what about putting the whole while loop into the try statement? Well, this depends on how you do it. If you just move the try keyword with the opening brace up and leave the closing brace in place, the compiler is not going to like it.





int main()
{ try {
while (true)
{ long numer, denom; double ans; // numerator/denominator
cout << MSG::msg(3) << MSG::msg(4); // prompt user for data
if ((cin >> numer >> denom) == 0) break; // enter data
fraction(numer,denom,ans); // compute answer
cout << MSG::msg(5) << ans <<"\n\n"; } // end of try
catch (char* str) // zero denominator
{ cout << str; }
catch (long val) // negative value
{ cout << MSG::msg(2) << val << "\n\n"; } } // end of loop
return 0; }



Now the scope of the try statement is not nested within the scope of the while loop. Whatever design decision you make, your scopes have to nest correctly; otherwise, the compiler becomes confused. In general, the more narrow the scope of the try statement, the better.



As these examples show, the design with exception handlers has to answer three basic questions:





  • where to throw an exception





  • where to catch the exception





  • what information to send to the exception handler





At the beginning of this chapter, I mentioned the popular rationale for using exceptions梥treamlining the client code through separation of the main line of processing from processing of exceptional cases. In this example, this rationale was of secondary importance at best. If anything, the client code became clogged with the try statement and the catch constructs with their parameters and braces.



It is just the other way around in the design with exceptions: You throw the exception in the place where you can discover the error and collect the data necessary for error recovery. You place the catch clauses in the place where the decision how to recover from the error can be made. In this simple example, this decision was just to skip the display of the answer. Still, it required sending data from the place of discovery to the place of recovery.





Claiming an Exception



Claiming exceptions is specifying what exceptions can be thrown within this function without handling it within the function itself, that is, what exceptions the function could pass to its caller. If the function does not catch the exception itself and expects some other function to deal with the problem, it has to declare (claim) the exception.



The keyword throw is used in claiming exceptions. Its general syntactic form combines the conventional function declaration, the keyword throw, and the list of types (in parentheses) whose values are being thrown by the function in search of the exception handler.





functionDeclaration throw (Type1, Type2, �TypeN);



Exceptions can be thrown by the function code implicitly, when an illegal condition occurs in a function call to its server function or explicitly by using the keyword throw.



If an exception is thrown by the function code and is caught in the function itself, there is no need to include it in the throw list. If the server function throws an exception and catches it, then this exception should not be included in the list. The list includes only those exceptions that the clients of this function will have to deal with.



For example, function inverse() in Listing 18.4 throws (and does not catch) two exceptions explicitly, a character array and a long. The definition of this function should include the throw keyword with these two types.





inline void inverse(long value, double& answer)
throw (char*, long)
{ answer = (value) ? 1.0/value : DBL_MAX;
if (answer==DBL_MAX)
throw MSG::msg(1); // explicit throw
if (value < 0)
throw value; } // explicit throw



Similarly, function fraction() in Listing 18.4 does not throw any explicit exceptions, but its server function inverse() throws (and does not catch) two exceptions. This means that function fraction() throws these two exceptions implicitly and should claim both of them.





inline void fraction (long numer, long denom, double& result)
throw (char*, long)
{ inverse(denom, result); // implicit throw
result = numer * result; } // result = numer/denom



If a function throws no exceptions at all, it can be declared with the empty throw specification throw(). For example,





void foo() throw (); // expect no exceptions



If a function does not define the exception specification, it might throw any exception.





void foo(); // no throw: expect any exception



It would be nice if it were an error in C++ to claim an exception that a function actually does not throw. It would also be nice if it were an error not to claim an exception that a function actually throws, either implicitly or explicitly. However, this is not the case, and you can get away with deceiving claims (claiming exceptions that the function does not throw) or inadequate claims (claiming only part of exceptions that the function throws) or blissfully ignoring the issue, as my Listing 18.4 amply illustrates.



Understanding somebody else's design of exception handling in the program could be a daunting adventure, and one might need all the help one could get. Claiming exceptions is a powerful technique for documenting design in code. Make sure that you use it wisely.



When a function processes exceptions only partially, this is reflected in how the function claims exceptions. Listing 18.5 demonstrates claiming of exceptions for a different division of responsibilities between functions inverse() and fraction(). While function inverse() throws (and claims) the same exceptions as in Listing 18.4, function fraction() handles the exception of type long itself. Hence, it claims only one exception in its interface, the character array.



This is why the main() function has to handle only one exception rather than two as in Listing 18.4. The output of a sample run of the program is presented in Figure 18-3.





Figure 18-3. Output for program in Listing 18.5.










Example 18.5. Example of claiming, throwing, and catching exceptions.


#include <iostream>
#include <cfloat>
using namespace std;

class MSG {
static char* data []; // internal static data
public:
static char* msg(int n) // public static method
{ if (n<1 || n > 5) // check index validity
return data[0];
else
return data[n]; } // return valid string
} ;

char* MSG::data [] = { "\nBad argument to msg()\n",
"\nZero denominator is not allowed\n\n", // depository of text
"\nNegative denominator: ",
"Enter numerator and positive\n",
"denominator (any letter to quit): ",
"Value of the fraction: "
} ;

inline void inverse(long value, double& answer)
throw (char*, long)
{ answer = (value) ? 1.0/value : DBL_MAX;
if (answer==DBL_MAX)
throw MSG::msg(1);
if (value < 0)
throw value; }

inline void fraction (long numer, long denom, double& result)
throw (char*)
{ try {
inverse(denom, result); } // result = 1.0 / denom
catch (long val) // negative value is OK
{ cout << MSG::msg(2) << val << "\n\n"; }
result = numer * result; } // result = numer / denom

int main()
{
while (true)
{ long numer, denom; double ans; // numerator/denominator
cout << MSG::msg(3) << MSG::msg(4); // prompt user for data
if ((cin >> numer >> denom) == 0) break; // enter data
try {
fraction(numer,denom,ans); // compute answer
cout << MSG::msg(5) << ans <<"\n\n"; } // valid answer
catch (char* str) // zero denominator
{ cout << str; }
}
return 0;
}


This example shows the advantage of claiming exceptions in function interfaces. When the client programmer wants to know what exceptions the client function should deal with, it is sufficient to inspect the claims of all server functions that this client function calls.





Rethrowing an Exception


Notice that the program behavior shown in Figure 18-3 is different from the behavior shown in Figure 18-2. In Figure 18-2, a negative value of the denominator is rejected, and the new input is requested from the user. In Figure 18-3, a negative value of the denominator is rejected, but the value of the result is printed anyway.



The reason for that is that the function fraction() recovers from this exception itself (by printing a message and the value of the denominator), and the main() function thinks that the result is valid and does not suppress its output.



This is a quite common situation, where the function can recover from the exception only partially, but some other action should be taken in one of its callers. C++ supports this need by allowing the function to rethrow the exception. This can be done by using the throw statement in the catch construct.



For example, function inverse() can avoid fooling main() into thinking that it completed the recovery by throwing the exception again.





inline void fraction (long numer, long denom, double& result)
throw (char*, long) // extra exception claim
{ try {
inverse(denom, result); } // result = 1.0 / denom
catch (long val)
{ cout << MSG::msg(2) << val << "\n\n";
throw val; } // throw it again
result = numer * result; }



Notice that this does not cause an infinite loop. The exception thrown in the catch construct scope cannot enter this scope梩o be able to do that, the exception should originate in the try block that precedes the catch construct. Formally, the exception is considered handled on entry into its exception handler. Hence, this throw statement will cause the search for another long error handler at a higher level, in the client code that called the function fraction().



Another way to rethrow the exception of the same type (and value) is just to say throw in the catch construct, and the exception specified in the parameter of the catch construct will be thrown again.





inline void fraction (long numer, long denom, double& result)
throw (char*, long) // extra exception claim
{ try {
inverse(denom, result); } // result = 1.0 / denom
catch (long val)
{ cout << MSG::msg(2) << val << "\n\n";
throw; } // same as "throw val"
result = numer * result; }




Listing 18.6 demonstrates this technique. Function inverse() is the same as in Listing 18.5. Function fraction() does partial processing of the long exception, but then it throws this exception again. This means that fraction() has to claim this exception in its interface, and main() has to provide the catch clause to handle this exception. If main() fails to do so, the program will terminate abnormally.



Since the only goal of throwing this exception again is to avoid the display of a result in main(), there is no processing that the catch construct for this exception should do in main(). This is why the body of the catch block is empty. It still should be there. To avoid generating a warning that the parameter of the catch construct is not used, I omit it from the parameter list, leaving only the type of the value. This is somewhat awkward but legitimate C++ technique.



The output of the program is shown in Figure 18-4. You see that the extraneous output is successfully suppressed.





Figure 18-4. Output for program in Listing 18.6.










Example 18.6. Example of rethrowing of an exception in a catch construct.


#include <iostream>
#include <cfloat>
using namespace std;
class MSG {
static char* data []; // internal static data
public:
static char* msg(int n) // public static method
{ if (n<1 || n > 5) // check index validity
return data[0];
else
return data[n]; } // return valid string
} ;

char* MSG::data [] = { "\nBad argument to msg()\n",
"\nZero denominator is not allowed\n\n", // depository of text
"\nNegative denominator: ",
"Enter numerator and positive\n",
"denominator (any letter to quit): ",
"Value of the fraction: "
} ;

inline void inverse(long value, double& answer)
throw (char*, long)
{ answer = (value) ? 1.0/value : DBL_MAX;
if (answer==DBL_MAX)
throw MSG::msg(1);
if (value < 0)
throw value; }

inline void fraction (long numer, long denom, double& result)
throw (char*, long)
{ try {
inverse(denom, result); } // result = 1.0 / denom
catch (long val) // negative value is OK
{ cout << MSG::msg(2) << val << "\n\n";
throw val; }
result = numer * result; } // result = numer / denom

int main()
{ cout << endl << endl;
while (true)
{ long numer, denom; double ans; // numerator/denominator
cout << MSG::msg(3) << MSG::msg(4); // prompt user for data
if ((cin >> numer >> denom) == 0) break; // enter data
try {
fraction(numer,denom,ans); // compute answer
cout << MSG::msg(5) << ans <<"\n\n"; } // valid answer
catch (char* str) // zero denominator
{ cout << str; }
catch (long) // just type
{ } // empty body
}
return 0;
}


This is a powerful technique for making several functions cooperate over processing the same exception. Use it with care, because at the root of this approach is the tearing apart (exception processing) of what probably should belong together. When it becomes difficult to concentrate exception processing in one place, programmers might be tempted to use this technique to make writing programs easier. It will most probably make understanding the code more difficult.








I l@ve RuBoard

No comments: