TOC BACK FORWARD

Charlie Calvert's C++ Builder Unleashed

- 5 -

Exceptions

In this chapter you learn how to add error handling to your programs. This is done almost entirely through a mechanism called exceptions.

In particular, the following subjects are covered:

To a large degree, BCB and the VCL make it possible for you to write programs that almost entirely ignore the subject of error checking. This is possible because exceptions are built into most classes and stand-alone routines and will be thrown automatically whenever something goes wrong. Furthermore, your entire program is automatically wrapped inside a try..catch block. Professional programmers, however, will want to go beyond even this level of safety and add additional error checking to their code, or else change the default error handling performed by BCB. Also, your programs might need to throw their own errors, so you will need to add and throw new exception classes.

This explanation of exceptions is not meant to replace chapters on that subject in standard books on C++ programming. Instead, I will concentrate on handling exceptions in VCL-based programs.

BCB does a great job of seamlessly surfacing Pascal-based VCL exceptions in your C++ code. As far as BCB programmers are concerned, there is no difference between an exception that occurs in a block of Pascal code and an exception that occurs in C++ code.

I believe exceptions are the correct model for reporting errors in any application. Because exceptions play such a big role in all VCL programs, this is a topic of which you should have at least a minimal understanding.

The Theory Behind Exceptions

Exceptions enable you to designate specific areas of your code designed to handle errors. In particular, you can "guard" whole sections of code in such a way that if errors occur inside them, the problem will be handled in a different area by a set of routines designed explicitly for that purpose. This technique covers nested functions, too, so you can begin a guarded block, step in through six or seven levels of function calls, and then, if something goes wrong, bounce directly back out to a single area in your code designed to handle error conditions. The goal is to do as much as possible to relieve the burden of writing error-handling routines so that you can concentrate on other, more important, goals.


NOTE: Right at the start, it's important to recognize the difference between BCB exceptions, which cover language issues, and hardware exceptions, which involve hardware and hardware interrupts. You can (and BCB often does) wrap a hardware exception inside a BCB exception and handle the event that way, but hardware exceptions are different than BCB exceptions.

For instance, running out of paper in a printer causes a hardware exception. Your code doesn't get an exception in this case, but a component might throw an exception if the situation is detected. Raising the exception does not cause the hardware error; it already exists outside the program.

Traditionally, error handling has been a matter of one area of code setting flags and then a second area of code responding appropriately. For instance, if you tried to open a file that does not exist, a flag would be set and other portions of your code could detect the condition by checking the flag. Under the new system, instead of a flag being set, an exception is thrown.

The easiest way to start appreciating the advantages that exceptions bring to your code is to imagine a situation in which three different levels of code are called. Suppose, for instance, that you wanted to display data in a grid, where the data in question is stored in a text file. You might have the following set of calls, all nested inside each other:

int DisplayDataInGrid();

  int RetrieveData();

    int OpenFile();

    int 
ReadFile();

    int CloseFile();

In this scenario, DisplayDataInGrid calls RetrieveData and RetrieveData calls OpenFile, ReadFile, and CloseFile. After CloseFile was called, control would return to DisplayDataInGrid, and the actual data would be shown to the user.

If something went wrong during the time the file was being opened, OpenFile would have to pass that information back to RetrieveData. RetrieveData would have to include code ensuring that ReadFile and CloseFile didn't get called; then it would have to pass the error condition back to DisplayDataInGrid. In turn, DisplayDataInGrid would have to ensure that it did not try to display data that was never retrieved correctly. This whole process of passing information in a daisy chain forces you to write complicated, confusing, and error-prone code.

As a child, you might have played a game called "telephone." In particular, you might have had a teacher, camp counselor, or friend who arranged a large group of people in a circle and then whispered something to the person on his or her left. That person in turn whispered the message to someone on his left, and so on, until the message went all the way around the circle. When the message had made the whole trip, its meaning almost always ended up changing radically from its initial conception. The same thing can happen in a program if you use the daisy chain method of conveying error conditions. The worst case, of course, is when one link in the chain is broken altogether and the rest of the code continues merrily on its way, oblivious of the fact that something serious has gone wrong.

Exceptions don't use the daisy chain theory of error processing. Instead, if an exception is thrown in the function OpenFile, the code automatically unwinds back to DisplayDataInGrid, where you can write code that handles the error. Exceptions automatically pop functions and data off the stack, and they automatically ensure that no other routines behind or in front of it are called until the code is found that is intended to handle the exception.


NOTE: In Java and Object Pascal, when code is being popped off the stack during an exception, the compiler checks for one particular block of code that needs to be executed, even if an exception has occurred. This special code is enclosed in try..finally blocks. C++ has no support for try..finally blocks. Instead, you can put your cleanup code in the destructor of your local objects. The destructor will be called even if an exception has occurred.

In BCB, if an exception occurs and you do not handle it explicitly, a default exception handler will process the exception. Most exceptions are not handled explicitly.

When the VCL default exception handler is invoked, it displays a message dialog describing the exception to the user, and then your program resumes its normal processing of Windows messages. Your program does not return to the code that threw the exception. When an exception is thrown, special compiler-generated code checks the most recently entered try block on the call stack for a suitable exception handler. If that try block has no exception handlers suitable for the current exception, the next outward try block is checked, and so on, until an exception handler is found or it reaches the default exception handler. Normal program execution resumes at the next statement following the code that handles the exception.

If you have a strong background in C++, it might take a while to get used to the idea that your VCL programs are automatically encased in exception-handling code. The standard C++ code you write will not partake of this benefit, but you can easily add exception handling to that code if you want it.


NOTE: Both RTTI and exception handling add bulk to your code. Your code is bigger and slower because it carries this burden. The subject of RTTI will be dealt with later in this chapter.

I personally believe that the exception-handling code is worth a small burden in terms of speed and size simply because of the degree of robustness that it adds to your code. Another important point to remember is that BCB's built-in exception handling eliminates the need for you to write a great deal of your own error-handling code. As a result, it actually produces a decrease in the size of your program. It is difficult to state with certainty whether the decrease in the size of your own code entirely offsets the increase in code size produced by BCB's built-in exception handling. In most situations, however, the two are likely to come close to canceling each other out.

I believe that if you combine the increased robustness given to you by exceptions with the benefit of having to write fewer lines of code, you come up with a clear winner. In fact, I would say that the VCL's built-in, exception-handling capabilities are one of the great benefits of the system.

As stated earlier, the VCL, and any related Object Pascal functions supported by BCB, are all guaranteed to be wrapped in exception handlers. This is not true of the standard C++ routines and objects that you might call. In my mind, this is a reason to turn to the VCL whenever possible, and to use other routines only when necessary. Exception handling is part of good contemporary programming practice, and I believe that it will remain part of it into the foreseeable future.

If the basic idea behind exceptions does not yet quite make sense to you, just forge ahead anyway, and I think it will become clear in time. The previous few paragraphs lay out the theory behind a fairly simple set of syntactical routines. Play with the syntax for a while, using the following descriptions as a guide, and then if you want, come back and read the theory a second time.

Exception Classes

BCB comes with a rich set of built-in exception classes meant for handling a wide range of exceptions. You can easily create your own exception classes for handling the key events in your program that might be susceptible to error. Here is the base class for BCB exception handling, as it is declared in SYSUTILS.HPP:

class __declspec(delphiclass) Exception;

class __declspec(pascalimplementation) Exception : public System::TObject

{

  typedef System::TObject inherited;

private:

  System::AnsiString FMessage;

  
int FHelpContext;

public:

  __fastcall Exception(const System::AnsiString Msg);

  __fastcall Exception(const System::AnsiString Msg,

    const System::TVarRec *Args, const int Args_Size);

  __fastcall Exception(int Ident);

  __fastcall 
Exception(int Ident, const System::TVarRec *Args,

    const int Args_Size);

  __fastcall Exception(const System::AnsiString Msg, int AHelpContext);

  __fastcall Exception(const System::AnsiString Msg,

    const System::TVarRec *Args,

    const 
int Args_Size, int AHelpContext);

  __fastcall Exception(int Ident, int AHelpContext);

  __fastcall Exception(int Ident, const System::TVarRec *Args,

    const int Args_Size, int AHelpContext);

  __property int HelpContext = {read=FHelpContext, 
write=FHelpContext, nodefault};

  __property System::AnsiString Message =

    {read=FMessage, write=FMessage, nodefault};

public:

  /* TObject.Destroy */ __fastcall virtual ~Exception(void) { }

};

A quick perusal of this declaration reveals that all exceptions have a message that can be displayed to the user. You can pass in this message through a number of different constructors, and you can retrieve it through the Message property.


NOTE: On the off-chance that you are new to C++, I might add here that C++ classes sometimes have multiple constructors because you may have more than one way in which you want to create a class. Sometimes you might want to initialize a class by passing in one type of string, and another time you might want to pass in a second type of string or perhaps an integer. To give you the flexibility you need, C++ enables you to declare multiple constructors. Needless to say, you still call only one constructor when creating a class; it's just that you have a choice of which constructor you want to choose. Unlike Object Pascal, all these constructors have the same name, which is identical to the class name.

Here are some additional built-in exceptions, all quoted directly from SYSUTILS.PAS:

class __declspec(delphiclass) EIntError;

class 
__declspec(delphiclass) EInOutError;

class __declspec(delphiclass) EOutOfMemory;

class __declspec(delphiclass) EDivByZero;

class __declspec(delphiclass) ERangeError;

class __declspec(delphiclass) EIntOverflow;

class __declspec(delphiclass) 
EMathError;

class __declspec(delphiclass) EInvalidOp;

class __declspec(delphiclass) EZeroDivide;

class __declspec(delphiclass) EOverflow;

class __declspec(delphiclass) EUnderflow;

class __declspec(delphiclass) EInvalidPointer;

class 
__declspec(delphiclass) EInvalidCast;

class __declspec(delphiclass) EConvertError;

class __declspec(delphiclass) EAccessViolation;

class __declspec(delphiclass) EPrivilege;

class __declspec(delphiclass) EStackOverflow;

class 
__declspec(delphiclass) EControlC;

class __declspec(delphiclass) EVariantError;

class __declspec(delphiclass) EPropReadOnly;

class __declspec(delphiclass) EPropWriteOnly;

class __declspec(delphiclass) EExternalException;

You can see that there are exception classes for divide-by-zero errors, file I/O errors, invalid type casts, and various other conditions both common and obscure.

The preceding list, however, is far from complete. Many other exceptions classes are declared in other modules of the VCL. To get a feeling for their complete scope, you should use the online help or browse the source files in the Include/VCL directory.

Basic Exception Syntax

When working with the code presented in this chapter, you will be raising a lot of exceptions. If you set Options | Environment | Preferences | Break on Exception to True, exceptions thrown by your program will cause the debugger to take you as near as possible to the place in your code where the exception occurred. Only after you start running again will you see the error message as it will be reported to the user. As a result, you should probably keep Break on Exception set to False except when you explicitly want to step through your code during an exception.

The SIMPEXP program, referenced in Listing 5.1, gives four examples of how to throw and handle exceptions in BCB. The first two examples shown are of the simplest possible kind, and are meant to get you started with the concepts involved in this process. The latter two examples in this program are a bit more complex, but are still relatively straightforward. The main form for the program is shown in Figure 5.1.

FIGURE 5.1. The main form for the SIMPEXP program. Remember to turn Optimizations off when running this program.

You can find the Optimizations settings in the Project | Options | Compiler menu option.

Listing 5.1. The SIMPEXP program demonstrates basic techniques for handling exceptions.

///////////////////////////////////////

// Copyright (c) 1997 by Charlie Calvert

//

#include <vcl.h>

#pragma hdrstop

#include "Main.h"

#include "codebox.h"

#pragma resource "*.dfm"


TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

}

void __fastcall TForm1::DelExceptClick(

  TObject *Sender)

{

  int i = 4, j = 0, k;

  k = i / j;



  ShowMessage(k);

}

void __fastcall 
TForm1::UserExceptClick(TObject *Sender)

{

  int i = 4, j = 0, k;

  try

  {

    k = i / j;

    ShowMessage(k);

  }

  catch(EDivByZero &E)

  {

    MessageDlg("Awe Shucks! Another division by zero error!",

               
mtError, TMsgDlgButtons() << mbOK, 0);

  }

}

void __fastcall TForm1::DeclareClick(TObject *Sender)

{

  int i;

  AnsiString S;

  try

  {

    i = StrToInt("Sam");

    ShowMessage(i);

  }

  catch(EConvertError &E)

  {

    
AnsiString S("Class where error occurred: " + this->ClassName());

    AnsiString S1("Type of error: " + E.ClassName());

    MessageDlg(S + `\r' + S1 + `\r' + E.Message, mtError,

      TMsgDlgButtons() << mbOK, 0);

  }


}

// Address2 String is in the CodeBox unit that ships w/ the book's CD

// in the UTILS subdirectory

void __fastcall TForm1::CalcAddClick(

  TObject *Sender)

{

  float k;

  try

  {

    k = StrToFloat("Sammy");

    ShowMessage(k);

  
}

  catch(EConvertError &E)

  {

    MessageDlg(E.Message + `\r' +

      " Error Address: " + Address2Str(ExceptAddr),

      mtError, TMsgDlgButtons() << mbOK, 0);

  }

}

This program contains four TBitBtns that, when pressed, throw exceptions. In the first case, the code lets BCB's built-in routines handle the exception; in the rest of the examples, a custom error handler is invoked.

Here is the simplest way to throw and handle an exception:

void __fastcall TForm1::DelExceptClick(

  TObject *Sender)

{

  int i = 4, j = 0, k;

  k = i / j;



  ShowMessage(k);

}

The code shown here causes a divide-by-zero error, and it will automatically pop up the dialog shown in Figure 5.2. Notice that the ShowMessage code never gets executed, because the exception occurs before you reach that part of your code. In this case, it would not cause a serious problem if ShowMessage were executed, but you don't want it to execute because it would report nonsense to the user. The benefit this code provides for you is therefore twofold:

FIGURE 5.2. The default mechanism for handling a divide-by-zero error.

The error message shown in Figure 5.2 is useful to programmers because it provides an address you can use in the Search | Find Error menu choice. However, it is not a particularly friendly message to send to a user of your program.

The following code demonstrates how to set up a try..catch block that gives you a place to handle an error:

void __fastcall 
TForm1::UserExceptClick(TObject *Sender)

{

  int i = 4, j = 0, k;

  try

  {

    k = i / j;

    ShowMessage(k);

  }

  catch(EDivByZero &E)

  {

    MessageDlg("Awe Shucks! Another division by zero error!",

               
mtError, TMsgDlgButtons() << mbOK, 0);

  }

}

The code that you want to test appears right after the reserved word try. In this case, all that goes on in this section is that you force a divide-by-zero error. When the error occurs, the code after the word catch is executed. In this section, you designate that you want to handle EDivByZero messages explicitly, and you do so by popping up an error message.

Once again, you will notice that the ShowMessage code is never executed, which is what you want, because it would only report nonsense to the user. Needless to say, the size of the block of code that needs to be skipped does not matter. Nor does it matter how deep into a series of function calls your code might go. For instance, instead of simply raising a divide-by-zero error on the top level, your code might call a second function. That function could in turn call another function, and so on, for six, ten, or however many levels of nested calls. If any of the routines in that block throw an EDivByZero error, the code would automatically jump to the MessageDlg shown in the preceding code.

Or, to state the same fact somewhat differently, if the code looked like this:

j = 0;

try

{

k = i / j;

DoSomething;

DoSomethingElse;

DoThis;

DoThat;

ShowMessage(k);

}

catch(EDivByZero &E)

{

MessageDlg("Awe Shucks! Another division by zero error!",

           
mtError, TMsgDlgButtons() << mbOK, 0);

}

then DoSomething, DoSomethingElse, DoThis, and DoThat would never be executed. Instead, the code would jump immediately from the division statement to the catch section.

You can combine the convenience of having BCB report an error with the luxury of being able to define your own error strings:

void __fastcall TForm1::DeclareClick(TObject *Sender)

{

  
int i;

  AnsiString S;

  try

  {

    i = StrToInt("Sam");

    ShowMessage(i);

  }

  catch(EConvertError &E)

  {

    AnsiString S("Class where error occurred: " + this->ClassName());

    AnsiString S1("Type of 
error: " + E.ClassName());

    MessageDlg(S + `\r' + S1 + `\r' + E.Message, mtError,

      TMsgDlgButtons() << mbOK, 0);

  }

}

The StrToInt function shown here will throw an EConvertError because it cannot convert the string "Sam" into an integer.

In this code, BCB is in effect enabling you to map an identifier onto the already existing exception object instance that was thrown. This is not a variable declaration, because no new storage is being allocated. It's simply giving you a convenient way to access the exception object instance so that you can extract additional information carried by the object instance, such as the error message string E.Message.

In this case, Message returns the string that BCB would associate with this error. To understand where Message comes from, refer to the declaration of TException shown previously. Note also that you need to work with the address of the EConvertError, because this is a VCL class and must be passed by reference:

catch(EConvertError &E)

This is what you want, anyway, because it is cheaper to pass around pointers than to pass around an actual copy of the object. You should not, however, allocate memory for an exception class before passing it, because that is a dangerous thing to do during an error condition.

After you have access to the message associated with an exception, you can add your own information to it. For instance, in this case I snag the name of the object whose method is raising the exception, as well as the name of the Exception class itself, thereby helping me know right away where the problem has occurred and why:

AnsiString S("Class where error occurred: " + this->ClassName());

AnsiString S1("Type of error: " + 
E.ClassName());

MessageDlg(S + `\r' + S1 + `\r' + E.Message, mtError,

TMsgDlgButtons() << mbOK, 0);

The string created in these lines of code is shown in Figure 5.3.

FIGURE 5.3. An exception that shows a string created in part by BCB and in part by the programmer.

You also might want to display the actual address at which an exception is thrown. The VCL ExceptAddr function returns this address:

void __fastcall TForm1::CalcAddClick(

  TObject *Sender)

{

  float k;

  try

  {

    k = StrToFloat("Sammy");

    ShowMessage(k);

  }

  
catch(EConvertError &E)

  {

    MessageDlg(E.Message + `\r' +

                  " Error Address: " + Address2Str(ExceptAddr),

                  mtError, TMsgDlgButtons() << mbOK, 0);

  }

}

This code displays the regular error message associated with an EConvertError exception, and then immediately converts the result of the ExceptAddr function into a string so that you can see that it returns the same address displayed by a BCB exception. You can use this address in the Search | Find Error menu option, which will take you to the place in your code where the error occurred.

To convert a pointer into a string, you can use the following function from the CodeBox unit:

AnsiString Address2Str(void *Addr)

{

  return Format(`%p', OPENARRAY(TVarRec, (Addr)));

}

This function uses the Format routine to perform a routine string operation that returns a string version of an address.

In this section, you learned how to handle simple exceptions. The actual logistics of handling these situations can be quite complex at times, but you are now armed with the basics needed to go into battle and wage war with the compiler. Note that I arranged the examples in this section in order of increasing complexity, where the first two are more typical of the code you will use in most programs, and the latter two are useful when you want to use advanced techniques.

Why Exceptions Are So Great

For some reason, when I first saw exceptions, I was confused by them. The syntax was simple enough, but I couldn't figure out exactly how I was supposed to use the darn things. My only consolation was that I saw so many others flounder in the same sea.

If you are feeling a little confused by what you have just seen of exceptions, try to cut through the fog created by their newness and remember that exceptions are really simple. They do two wonderful things:

1. Pop up error messages automatically.

2. Help you avoid accidentally executing any sensitive code after an error occurs.

If you are confused by an exception, try asking these two questions:

1. What error is being reported for me automatically?

2. What sensitive code is or can be protected by this exception?

In the preceding examples, the answer to the first question is this: either an EDivByZero or EConvertError message is being reported automatically, without my necessarily having to do any work. The answer to the second question is that a call to ShowMessage is being protected. Therefore, ShowMessage will only be called when it has valid data to show the user.

That's it! If you can grasp those two simple concepts, you know the most important facts about exceptions. At their core, exceptions are very simple and very useful.

Given the extreme simplicity and extraordinary benefits associated with this subject, there is only one big question left: Why doesn't everyone use exceptions all the time? Why do so many people regard this simple and helpful tool as something rather advanced and murky?

The biggest impediment to the acceptance of exceptions is simply that people haven't yet gotten in the habit of wrapping their entire program inside a try..catch block. If you don't take that one step, exceptions can be a minefield that might cause your program to shut down unexpectedly. However, if you do wrap your whole program in a well-constructed try..catch block, this tool will do an incredible amount of good work for you with little effort on your part.

One of the great benefits of BCB is that all standard BCB programs automatically exist inside a well-constructed try..catch block. (Console applications don't necessarily have this same benefit.) Furthermore, the entire VCL, and most of the routines that support it, also make good use of exceptions. For instance, the StrToInt function you saw earlier throws its own exceptions. But I am getting ahead of myself, because there is more territory to cover before talking about raising your own exceptions.


NOTE: It's important to understand that you should not use exceptions indiscriminately. There is a difference between wrapping a program in a try..catch block and filling it up with thousands of try..catch blocks. There is some overhead involved in this process, so you should use it with a degree of discretion.

The main point is to understand that exceptions are at heart very simple. Furthermore, they exist primarily to make your life as a programmer considerably simpler.

Throwing Exceptions

I want this part of the book to focus primarily on what BCB and the VCL bring to standard exception-handling practice. However, it might be helpful to show a few bits of syntax that highlight some of the particular features of C++ exception handling.

Consider the SimpleExcept program (distinct from the SIMPEXP program shown previously) found in Listing 5.2. This program highlights some of the basic facts about exceptions in C++. For an in-depth discussion of this subject, you should turn to a book on the C++ language. The main form for the SimpleExcept program is shown in Figure 5.4.

FIGURE 5.4. The SimpleExcept program raises many different kinds of exceptions.

Listing 5.2. The SimpleExcept program shows a few basic features of C++ exception-handling syntax.

///////////////////////////////////////

// File: Main.cpp

// Project: SimpleExcept

// Copyright (c) 1997 by Charlie Calvert

/*

  This code provides a few very 
simple examples of basic

  exception syntax.

*/

#include <vcl\vcl.h>

#pragma hdrstop

#include "Main.h"

#pragma resource "*.dfm"

TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

}

void 
__fastcall TForm1::IntegerExecptBtnClick(TObject *Sender)

{

  try

  {

    throw 23;

  }

  catch(int ErrorCode)

  {

    ShowMessage(ErrorCode);

  }

}

void __fastcall TForm1::ExceptionClassBtnClick(TObject *Sender)

{

  try

  {

    int i = 
StrToInt("Sam");

  }

  catch(Exception &A)

  {

    ShowMessage("Info: " + A.Message);

  }

}

void __fastcall TForm1::MultiCatchBtnClick(TObject *Sender)

{

  try

  {

    int i = StrToInt("Sam");

  }

  
catch(int ErrorCode)

  {

    ShowMessage("int: " + ErrorCode);

  }

  catch (WORD Sam)

  {

    ShowMessage("WORD: " + AnsiString(Sam));

  }

  catch(Exception &A)

  {

    ShowMessage("Exception: " + 
A.Message);

  }

  catch(EConvertError &B)

  {

    ShowMessage("EConvertError: " + B.Message);

  }

  catch(EDatabaseError &C)

  {

    ShowMessage(C.Message);

  }

}

void __fastcall TForm1::GenericCatchBtnClick(TObject 
*Sender)

{

  try

  {

    StrToInt("Sam");

  }

  catch (...)

  {

    ShowMessage("Something went wrong");

  }

}

void __fastcall TForm1::GenericCatchIIBtnClick(TObject *Sender)

{

  try

  {

    throw 12;

  }

  
catch(EExternalException &E)

  {

    ShowMessage(E.Message);

  }

}

This very simple-minded program shows some of the things you can do with exceptions in C++.

Here is a very simple-minded way to throw an exception:

void __fastcall TForm1::IntegerExecptBtnClick(TObject *Sender)

{

  try

  {

    throw 23;

  }

  catch(int ErrorCode)

  {

    ShowMessage(ErrorCode);

  }

}

The throw statement shown here causes an exception to be thrown. The catch statement catches the error. Needless to say, the variable ErrorCode will be set to the integer value thrown during the exception, which is 23.

The problem with this example is that it is so stripped-down as to appear a bit mysterious. The first point to grasp when looking at this code is that exceptions are not caused by errors--they are caused by using the throw keyword. When you use exceptions, the errors that occur are still the same old boring errors that C++ programmers have been looking at for years. The only difference here is that after the exception occurs, you can notify the user of your routine or object that something has gone wrong by using the keyword throw.

The simplest type of object you can throw is of type int, as shown here. Of course, as you have already seen, it is usually more helpful to throw an object rather than a simple integer. The advantage, of course, is that objects can speak to you and can be queried, while integers are a bit mute and faceless.

Compare the previous IntegerExceptionButtonClick with this method:

void __fastcall 
TForm1::ExceptionClassBtnClick(TObject *Sender)

{

  try

  {

    int i = StrToInt("Sam");

  }

  catch(Exception &A)

  {

    ShowMessage("Info: " + A.Message);

  }

}

Code inside of StrToInt throws an exception if an error occurs. If it did not explicitly use the throw keyword (or at least its Pascal equivalent), the exception would not occur. There is nothing mysterious about exceptions; they occur because someone used a throw expression in their code.

You have seen that you can pass different types of variables to a catch statement:

void __fastcall TForm1::MultiCatchBtnClick(TObject *Sender)

{

  try

  {

    
int i = StrToInt("Sam");

  }

  catch(int ErrorCode)

  {

    ShowMessage("int: " + ErrorCode);

  }

  catch (WORD Sam)

  {

    ShowMessage("WORD: " + AnsiString("Sam"));

  }

  catch(Exception &A)

  
{

    ShowMessage("Exception: " + A.Message);

  }

  catch(EConvertError &B)

  {

    ShowMessage("EConvertError: " + B.Message);

  }

  catch(EDatabaseError &C)

  {

    ShowMessage(C.Message);

  }

}

This code is loaded down with a series of catch statements:

catch(int ErrorCode)

catch (WORD Sam)

catch(Exception &A)

catch(EConvertError &B)

catch(EDatabaseError &C)

The question is which statement, or statements, will be called?

The first part of the answer is that only one catch statement will be called unless you rethrow the error. I will talk about reraising errors later in the chapter. The second half of the question, however, still remains. Which statement will execute?

As you know, the error which occurs in this program is of type EConvertError:

int i = StrToInt("Sam");

Given the nature of the error, it should be obvious that neither of the first two options will be executed. The object thrown is not of type int, and it is not of type WORD. It therefore will bypass these two catch statements with nary a nod.

You might think, however, that the code would also skip the Exception catch block and go directly to the EConvertError exception. This is not what happens. Instead, the Exception catch block is executed because the rules of polymorphism dictate that an EConvertError can be assigned to one of its parent objects. As you saw earlier, EConvertError is a child of Exception.

Given this information, the logical way to arrange the code in this method is as follows:

catch(int ErrorCode)

catch (WORD Sam)

catch(EConvertError &B)

catch(EDatabaseError &C)

catch(Exception &A)

This code will first try to find a match in the first four catch statements, and if that fails, it will come to rest in the Exception catch statement, because that is a generic resting place for VCL exceptions.

Of course, not all exceptions are VCL exceptions. For instance, exceptions of type WORD or int are not VCL exceptions.

There are some things you can do to spread a really wide net for catching exceptions. The following examples show how to use the ellipses syntax in a catch expression to trap generic exceptions of any type:

void __fastcall TForm1::GenericCatchBtnClick(TObject *Sender)

{

  try

  {

    StrToInt("Sam");

  }

  
catch (...)

  {

    ShowMessage("Something went wrong");

  }

}

This catch block will trap whatever exception comes its way. It is the broadest and safest possible net, but it is also not very helpful when it comes to telling you exactly what went wrong. To flag down at least some additional information, you can include except.h in your program and use the following syntax:

void __fastcall 
TForm1::GenericCatchBtnClick(TObject *Sender)

{

  try

  {

    throw 12;

  }

  catch (...)

  {

    ShowMessage("Something went wrong");

    ShowMessage(__throwExceptionName);

  }

}

This code uses throwExceptionName to find the name of the type of exception that was thrown. In this case, it will report that you have an exception of type int. Not a great or wondrous fact, but at least it is a start if you are trying to figure out what went wrong. __throwExceptionName might not work with VCL exceptions, but it will work with standard C++ exceptions.

Here is a VCL exception class that might be of some use when you are trying to track down exceptions:

void __fastcall TForm1::GenericCatchIIBtnClick(TObject *Sender)

{

  try

  {

throw 12;

  }

  catch(EExternalException &E)

  {

    ShowMessage(E.Message);

  }

}

This VCL exception class will handle the error that has occurred, even though it is not a standard VCL error.

I've walked right up to the edge of the point beyond which I do not want to go in this book. From here, keen-sighted readers can no doubt catch sight of some generic C++ vistas that contain innumerable questions concerning subtle matters of correct syntactical usage. For more information, you should turn to a book on the C++ language. My goal in this book is to stick to simple syntax that is easy to use, and to avoid digging into potential minefields, no matter how interesting the terrain may appear.

My suggestion is to not yield to the temptation to do anything fancy with exceptions. Just use them to report errors, and do so by throwing standard VCL exception classes or descendants of standard VCL exception classes. If you follow these rules, you will reap benefits and avoid trouble.

Throwing VCL Exception Classes

Here is an example from the SimpleException program of how to throw a VCL exception:

void __fastcall TForm1::ThrowVCLExceptionClick(TObject *Sender)

{

  try

  {

    throw Exception("VCL class");

  }

  
catch(Exception &E)

  {

    ShowMessage(AnsiString(E.ClassName()) + " " + E.Message);

  }

}

The throw statement in this method automatically creates an instance of the Exception class and calls its constructor with a simple string. You can, depending on your needs, place anything in this string.

Notice that there are other constructors for automatically formatting strings and for retrieving strings that are stored in the program's executable. For instance, if you have a string table, you can automatically retrieve an item from that table by number using the following constructor of the Exception class:

__fastcall Exception(int Ident);


In this code, Ident is the ID of a string in a resource linked into your application.

Here is a constructor designed to enable you to format input with a string stored in a resource:

__fastcall Exception(int Ident, const System::TVarRec * Args, const int Args_Size);

For instance, here is an example of using a string from a string table in conjunction with a format statement:

throw Exception(12, OPENARRAY(TVarRec, (12, "Value out of range!")));

If the string stored in the resource table looked like this

Custom Error %d: %s

then the string shown to the user would be

Custom Error 12: Value out of range!

An example of raising this kind of error is found in the ResError program, discussed later in this chapter. The source for the program is found in the ResError directory of the CD-ROM that accompanies this book.

Understanding the VCL Exception Classes

In the section of this chapter called "Exception Classes," I showed you a long, but not exhaustive, list featuring the names of a number of VCL exception classes. Here, to refresh your memory, are some of the declarations from that list:

class 
__declspec(delphiclass) EIntError;

class __declspec(delphiclass) EInOutError;

class __declspec(delphiclass) EOutOfMemory;

class __declspec(delphiclass) EDivByZero;

The interesting thing about all these classes is that they descend from a single root class called Exception. The declaration for class Exception was quoted in full in the section of this chapter called "Exception Classes."

I've already said that one of the great things BCB does for you is wrap your whole program in a try..catch block. A second great BCB benefit, of almost equal importance, is this class hierarchy. Of course, none of this would matter if C++ was not such a strong language with the capability to support a wide range of powerful features.

As you have seen, it's possible to create exceptions that pass nothing but an integer to a catch block:

catch (int)

{

}

This syntax can be useful under certain circumstances, but you are much better off if you receive an entire exception class inside a catch block. That way you can call the methods of that class in order to find out exactly what is wrong, or to perform other tasks that might be helpful to you.

For instance, if you are not sure of the exact name of the error class which is being thrown, you can always throw the error intentionally and check its ClassName inside a catch block that catches all VCL exceptions:

catch(Exception &E)

{

AnsiString S(this->ClassName());

AnsiString S1(E.ClassName());

MessageDlg(S + `\r' + S1, mtError, TMsgDlgButtons() << mbOK, 0);

}

This code reports the class in which the exception occurred, and then on the next line, the class that threw the exception. For instance, it might post the following lines inside the MessageDlg:

TForm1

EConvertError


The key point here is that polymorphism is being put to work to the programmer's great benefit. All VCL exception classes descend from class Exception, so the rules of polymorphism dictate that you can pass them to a catch block that takes a parameter of type Exception. Furthermore, when you call the methods of the class passed to you, polymorphism will ensure that the method called is not necessarily of type Exception, but of the type passed to you.

The previous code correctly reports that the class name of the exception is EConvertError, even though the type of the class is declared to be of type Exception. In other words, due to the wondrous rules of polymorphism, an exception of type EConvertError could be passed to you through a variable of type Exception.

Creating and Raising Your Own Exceptions

In this section, I show how you can create your own exceptions and how to throw exceptions when errors occur. The code explored in this section is from the MyExcept program shown in Listing 5.3. The form for the program has three buttons and an edit control. The main form for the MyExcept program is shown in Figure 5.5.

FIGURE 5.5. The main form for the MyExcept program.

Listing 5.3. The header for the MyExcept program shows how to create a custom exception.

///////////////////////////////////////

// Main.h

// Project: MyExcept


// Copyright (c) 1997 by Charlie Calvert

//

#ifndef MainH

#define MainH

#include <Classes.hpp>

#include <Controls.hpp>

#include <StdCtrls.hpp>

#include <Forms.hpp>

#include <Buttons.hpp>



class 
ESillySpellingError: public Exception

{

  public:

  __fastcall ESillySpellingError(const AnsiString Msg)

    :Exception(Msg) {}

};



class TForm1 : public TForm

{

__published:

  TLabel *Label1;

  TBitBtn *ERead;

  TBitBtn *EReadAddr;

  
TBitBtn *RaiseException;

  TEdit *Edit1;

  void __fastcall RaiseExceptionClick(

  TObject *Sender);

  void __fastcall EReadClick(

  TObject *Sender);

  void __fastcall EReadAddrClick(

  TObject *Sender);

private:

public:

  virtual __fastcall 
TForm1(TComponent* Owner);

};



extern TForm1 *Form1;



#endif

Listing 5.4. The MyExcept program shows how to throw standard and custom exceptions.

///////////////////////////////////////

// Main.cpp

// Project: MyExcept

// Copyright (c) 1997 by Charlie Calvert

//

#include <vcl.h>

#pragma hdrstop

#include "Main.h"


#include "codebox.h"

#pragma resource "*.dfm"



TForm1 *Form1;



__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

}



void __fastcall TForm1::RaiseExceptionClick(

  TObject *Sender)

{

  
StrToInt("23a");

}



void __fastcall TForm1::EReadClick(

  TObject *Sender)

{

  throw EReadError("EReadError has occurred");

}

AnsiString GetAddr()

{

  void *P;

  P = Form1->MethodAddress("EReadAddrClick");

  
return Address2Str(P);

}



void __fastcall TForm1::EReadAddrClick(TObject *Sender)

{

  AnsiString S = Edit1->Text;

  if (UpperCase(S) == "OCCURED")

  {

    S = "A Silly Spelling error has occured! "

        "The 
correct spelling is occuRRed, not occuRed! ";

    throw ESillySpellingError(S + "\rAddress: " +GetAddr());

  }

}

To use this program, just press any of the three buttons on the form. The third button won't throw an exception unless the edit control is set to the misspelled string occured.

Exceptions occur because they are explicitly thrown. For instance, here is another version of the StrToInt routine:

int Str2Int(AnsiString S)

{

  int i = atoi(S.c_str());

  if (i == 0)

    throw EConvertError("Cannot convert " + S + " to an int");

  return i;

}

This function uses the atoi function to convert a string into an integer. If all is successful, the function result is set equal to the transmuted string. If there is a problem, the value returned from atoi is zero. When an error condition exists, the Str2Int routine throws an EConvertError and passes in a string that explains what has gone wrong.


TIP: Obviously, the previous function would not work correctly if you passed in "0" or "00", and so on, as a string. If you need a secure function, use StrToInt.

EConvertError is a built-in BCB type meant to be used in situations where an error occurs in a conversion routine. If you look in SYSUTILS.HPP, you will find that BCB uses this exception quite a bit, but tends to throw it through the good graces of the ConvertError routine.

There is nothing in BCB that forces you to use a particular class of exceptions in a particular situation. For instance, in the following method I throw an EReadError, even though nothing has gone wrong in the program:

void __fastcall TForm1::EReadClick(TObject *Sender)

{

  throw EReadError("EReadError has occurred");

}

As you saw earlier, exceptions are triggered when you use the reserved word throw and then construct an instance of a particular type of exception. In and of themselves, exceptions have nothing to do with errors, and indeed you could use them for some entirely different purpose. In other words, exceptions are a good means of reporting errors, but they do not occur because an error occurs; they occur because you use the reserved word throw!

Many of the exception classes that are built into BCB may be useful to you at times. For instance, you might need to convert some variable from one type to another; or there may be an occasion when you need to read some value. If errors occur during such tasks, it would make sense to throw an EConvertError or an EReadError. That way, the code that depends upon your conversion routines doesn't have to worry about what to do when the conversion routines fail. On failure, they will throw an exception and will never return bad data or error codes to the caller. That can go a long way toward simplifying your code.

Despite the usefulness of many of BCB's built-in exception classes, there are many occasions when you are going to need to create exception classes of your own. To do so, you should first declare a new class:

class ESillySpellingError: public Exception

{

  public:

  __fastcall ESillySpellingError(const AnsiString Msg)

    :Exception(Msg) {}

};

This code states that class ESillySpellingError is a descendant of type Exception. You can create as many constructors for your class as you think you need, or you can simply copy the constructors from one of the classes in the SYSUTILS.HPP file and do a search and replace on the class name.

I created ESillySpellingError for the quixotic reason of desiring to finally squelch permanently a spelling error that I have made many times in my life. In particular, I tend to misspell the past tense of the word occur:

void __fastcall TForm1::EReadAddrClick(TObject *Sender)

{

  AnsiString S = Edit1->Text;

  if (UpperCase(S) == "OCCURED")

  {

    S = "A Silly Spelling error has occured! "

        "The 
correct spelling is occuRRed, not occuRed! ";

    throw ESillySpellingError(S + "\rAddress: " +GetAddr());

  }

}

Hopefully, writing about the error in this book will help me remember that there are two Rs in the word--not just one! (My plan seems to have worked, but alas, there are so many other words that I misspell!)

At any rate, you can see that it is easy to create your own exceptions and to throw them. Whenever you feel that you need to describe a new type of error, you can do so by just creating the simple type shown in the preceding code. If you want, you can create more complex exception types. For instance, the BCB EInOutError adds an ErrorCode to its object declaration. You can then reference this error code in a catch block:

try

  ..

catch(EInOutError &E)

{

  Code = E.ErrorCode;

}

Remember that the need for different types of errors becomes evident not when you are raising them, but in the catch portion of try..catch blocks. You might want to handle a particular type of error explicitly and let other errors be handled by the default handler. To do this, you must have a variety of different exception classes to throw so that your exception handlers can be set up to distinguish between the different error situations.

The lazy man's way to raise an exception is simply to write

throw 
Exception("Another lazy man error has occured!");

This technique works fine in many cases. However, it is best to create your own exception classes so that you can create try..catch blocks that work only with a specific type of error.

For more on creating your own exception classes, see the ResError program shown later in this chapter. ResError has an example in it of using a constructor for a custom Exception class that grabs a string from a string table and uses the format function to customize the string when displaying it to the user.

Rethrowing an Exception

The AutoThrowClick method from the ThrowAgain program shows how to rethrow an exception:

void __fastcall TForm1::ThrowItAgainSamClick(TObject *Sender)

{

  int k;

  try

  {

    k = StrToInt("Sam");

    ShowMessage(k);

  }

  catch(...)

  {

    
ShowMessage("Something went wrong");

    throw;

  }

}

Rethrowing an exception involves nothing more than using the throw keyword. The preceding example intentionally creates an EConvertError error, shows a message to the user, and rethrows the exception. If you run the program, you will first see the custom error message I have devised and then the standard EConvert error message. Most of the time you would not want to show two error messages, and I have written this kind of code only so you can see exactly what is happening when you run the program.

Rethrowing an exception enables you to get the best of both worlds. You get to perform some custom handling and allow BCB to automatically inform the user exactly what has gone wrong.

In many cases, it is a good idea to rethrow an exception, because you cannot be sure that all the handling necessary for dealing with the error is complete. In fact, it is often a good idea to let the system handle exceptions automatically without interfering in any way. If you do need to step into the process by writing a try..catch block, you should give serious thought to rethrowing the exception in case some other routine needs to know about it. The great beauty of exceptions is that things often work best if you just forget about handling errors altogether!

Exceptions and Destructors

Despite the note on which I ended the last section, there are certain types of situations in which you need to ensure that a particular block of code is executed even if something goes wrong in the code that precedes it. For instance, you may allocate memory, perform several actions, and finally intend to deallocate the memory. However, if an exception is thrown between the time you allocate memory and the time you want to deallocate the memory, the code that deallocates the memory might never get executed. In Java and Object Pascal, this situation is handled with try..finally blocks. C++ does not support that syntax, but it has another technique you can use instead.

Here is a second way to think about this whole situation. As you know, a try..catch block can cause the execution pointer for your program to jump from the place that an error occurs directly to the place where you handle that error. That is all well and good under most circumstances, and indeed, as I said at the end of the last section, it is usually best to let this process occur unattended. However, there are times when you want to make sure that some code between the error and the exception handler is executed regardless of circumstances. The code shown in Listings 5.5 through 5.7 shows how to proceed. The program is run in a simple form that contains a button and an edit control.

Listing 5.5. The AllObjects program shows how to handle memory allocations.

///////////////////////////////////////

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef MainH

#define MainH

#include <Classes.hpp>

#include 
<Controls.hpp>

#include <StdCtrls.hpp>

#include <Forms.hpp>

class TMyObject

{

public:

  TMyObject(void);

  ~TMyObject(void);

  void SayHello(void);

  void BlowUp(void);

};

class TForm1 : public TForm

{

__published:

  
TButton *NoCareTaker;

  TButton *CareTaker;

  TButton *CareTakeWithTryExcept;

  void __fastcall NoCareTakerClick(

  TObject *Sender);

  void __fastcall CareTakerClick(

  TObject *Sender);

  void __fastcall CareTakeWithTryExceptClick(

  TObject 
*Sender);

private:

public:

  virtual __fastcall TForm1(TComponent* Owner);

};

extern TForm1 *Form1;

#endif

Listing 5.6. The AllObjects program shows how to handle memory allocations (continued).

///////////////////////////////////////

// Copyright (c) 1997 by Charlie Calvert

//

#include <vcl.h>

#pragma hdrstop

#include "Main.h"

#include 
"CareTaker1.h"

#pragma resource "*.dfm"

TForm1 *Form1;

TMyObject::TMyObject(void)

{

  ShowMessage("TMyObject constructor called");

}

TMyObject::~TMyObject(void)

{

  ShowMessage("TMyobject destructor 
called!");

}

void TMyObject::BlowUp(void)

{

  throw(42);

}

void TMyObject::SayHello(void)

{

  ShowMessage("MyObject says hello");

}

__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

}

// In this method the 
destructor for TMyObject is NOT called

void __fastcall TForm1::NoCareTakerClick(TObject *Sender)

{

  TMyObject *MyObject = new TMyObject;

  MyObject->SayHello();

  MyObject->BlowUp();

  delete MyObject;

}

// In this method the destructor 
for TMyObject is called

void __fastcall TForm1::CareTakerClick(

  TObject *Sender)

{

  TCareTaker<TMyObject> MyCareTaker;

  TMyObject *MyObject = MyCareTaker.GetObject();

  MyObject->SayHello();

  MyObject->BlowUp();

}

void 
__fastcall TForm1::CareTakeWithTryExceptClick(

  TObject *Sender)

{

  TCareTaker<TMyObject> MyCareTaker;

  TMyObject *MyObject = MyCareTaker.GetObject();

  try

  {

    MyObject->SayHello();

    MyObject->BlowUp();

  }

  catch(int 
i)

  {

    ShowMessage("The secret of the universe revealed: " + AnsiString(i));

  }

}

Listing 5.7. The caretaker module contains a useful template class that ensures the memory allocated for an object will be destroyed.

///////////////////////////////////////

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef CareTaker1H

#define CareTaker1H

template<class T> class 
TCareTaker

{

  T* Pointer;

public:

  TCareTaker()

  {

    ShowMessage("CareTaker constructor called.");

    Pointer = new T;

  }

  ~TCareTaker(void)

  {

    ShowMessage("CareTaker destructor called.");

    delete 
Pointer;

  }

  T *GetObject(void)

  {

    return Pointer;

  }

};

#endif

The key point to grasp about the AllObjects program is that it consists entirely of objects. This is important because of a simple rule of C++ exception handling: Local objects whose constructors are called successfully before an exception occurs will have their destructor called.


NOTE: The word "successfully" in the last sentence of the previous paragraph is very important! This system won't work if an exception occurs midway through the call to a constructor. Again, I am on the edge of entering a dark and murky C++ underworld. Rather than burden myself with pages of exegesis on the correct way to cross the river Styx, I will step away from this subject by simply advising you not to allow exceptions to occur inside a constructor. If you have something delicate to do, don't do it in a constructor! It's not that C++ has no means of dealing with that type of situation; I just think it's wiser to play it safe. The goal is to get programs written, not to find the Shadow Land and spend time poking around therein!

The thing I like about this rule regarding exceptions and destructors is that it encourages programmers to make everything an object. I've been using objects pretty much every day for the last eight or nine years, and the more I see of them, the more I like them. Objects are the right paradigm, and coding practices that encourage their use are probably worth cultivating.

The AllObjects program contains a simple object called TMyObject:

class TMyObject

{

public:

  TMyObject(void);

  ~TMyObject(void);

  void SayHello(void);

  void BlowUp(void);

};

The implementation for this object is very straightforward:

TMyObject::TMyObject(void)

{

  ShowMessage("TMyObject constructor called");

}

TMyObject::~TMyObject(void)

{

  ShowMessage("TMyobject destructor called!");

}

void TMyObject::BlowUp(void)

{

  throw(42);

}


void TMyObject::SayHello(void)

{

  ShowMessage("MyObject says hello");

}

The constructor and destructor simply report on whether or not they have been called. The SayHello method just pops up a simple message. The BlowUp method raises an exception.

I use the TMyObject class three times in the program. The first time, I call it in order to show the problem that needs to be fixed:

void __fastcall 
TForm1::NoCareTakerClick(TObject *Sender)

{

  TMyObject *MyObject = new TMyObject;

  MyObject->SayHello();

  MyObject->BlowUp();

  delete MyObject;

}

This method allocates an instance of TMyObject on the heap, calls SayHello, and finally steps into the whirlwind by calling BlowUp. BlowUp will raise an exception, which means that the code executing after the call to BlowUp will never be called. In particular, the memory allocated for MyObject will never be freed.

The solution to this problem is simply to put the sensitive code you want to have executed inside the destructor for a local object whose constructor was called before the exception occurs.

One generic way to solve this type of problem is to create a bare-bones template class that handles memory allocation:

template<class T> class TCareTaker

{

  T* Pointer;

public:

  
TCareTaker()

  {

    ShowMessage("CareTaker constructor called.");

    Pointer = new T;

  }

  ~TCareTaker(void)

  {

    ShowMessage("CareTaker destructor called.");

    delete Pointer;

  }

  T *GetObject(void)

  {

    
return Pointer;

  }

};

#endif

I called this class TCareTaker because it takes care of memory allocations and deallocations for you. Calling the class is extremely simple:

void __fastcall 
TForm1::CareTakerClick(TObject *Sender)

{

  TCareTaker<TMyObject> MyCareTaker;

  TMyObject *MyObject = MyCareTaker.GetObject();

  MyObject->SayHello();

  MyObject->BlowUp();

}

The first line of code allocates memory for an instance of TMyObject. The second line retrieves the instance, and the rest of the code calls the methods of the object. There is no need to explicitly deallocate the memory used by TMyObject, because it will be deleted for you automatically by TMyCareTaker the moment before TMyCareTaker goes out of scope:

~TCareTaker(void)

{

ShowMessage("CareTaker destructor called.");

delete Pointer;

}

Notice also that it does not take any more lines of code for you to use this technique than it does to allocate the memory for TMyObject the normal way. Of course, there is overhead associated with this process, but it does not take up any more of your time to use it.

The important part of this code hangs on the fact that TCareTaker is a template with a destructor. The fact that TMyObject is also a class is largely irrelevant to the main argument of this example. I made TMyObject a class simply so you can visibly see its destructor being called when you step through the code or run the program. The important object in this example, however, is the minimal implementation of TCareTaker.

Streams, Exceptions, and Freeing Memory

In the last section you learned the basics about how to use the destructor of an object to ensure that certain code will be executed even after an exception has been thrown. The CareTaker example used to illustrate the point is valuable in that it shows how to handle memory allocations. However, there are other situations besides memory allocations where you need to ensure that code will be executed.

The example presented in this section still involves memory allocation, but it also focuses on the issue of ensuring that files are properly opened and closed even if an exception occurs. The source for the program is shown in Listings 5.8 and 5.9.

The main reason I am providing this second example of how to use destructors to your advantage in exception handling is simply that it is a relatively "classic" example of how to wrap up a process inside an object. Here, the delicate act of opening and closing a file could become troublesome, so the program wraps the process up inside an object.

Listing 5.8. The StreamException program shows how to guard code involving streams.

///////////////////////////////////////

// Main.h

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef MainH

#define MainH

#include <Classes.hpp>


#include <Controls.hpp>

#include <StdCtrls.hpp>

#include <Forms.hpp>

class TFileObject

{

  AnsiString FFileName;

  TFileStream *FMyFile;

public:

  TFileObject(AnsiString S);

  ~TFileObject();

  void OpenFile();

  
AnsiString GetData(AnsiString &S);

  void BlowUp();

};

class TForm1 : public TForm

{

__published:

  TButton *Button1;

  TMemo *Memo1;

  void __fastcall Button1Click(

  TObject *Sender);

private:

public:

  virtual __fastcall 
TForm1(TComponent* Owner);

};

//--------------------------------------------------------------------------

extern TForm1 *Form1;

//--------------------------------------------------------------------------

#endif

Listing 5.9. The StreamException program shows how to guard code that performs file IO.

///////////////////////////////////////

// Main.cpp

// Copyright (c) 1997 
by Charlie Calvert

//

#include <vcl.h>

#pragma hdrstop

#include "Main.h"

#pragma resource "*.dfm"

TForm1 *Form1;

///////////////////////////////////////

// TFileObject ////////////////////////


///////////////////////////////////////

TFileObject::TFileObject(AnsiString S)

{

  FFileName = S;

  FMyFile = NULL;

}

TFileObject::~TFileObject()

{

  ShowMessage("Freeing memory associated with stream!");

  FMyFile->Free();

}


void TFileObject::OpenFile()

{

  FMyFile = new TFileStream(FFileName, fmOpenRead);

}

AnsiString TFileObject::GetData(AnsiString &S)

{

  S.SetLength(FMyFile->Size + 1);

  FMyFile->Read(S.c_str(), FMyFile->Size);

  
S[FMyFile->Size] = `\0';

  return S;

}

void TFileObject::BlowUp()

{

  throw EInOutError("Exception raised in method BlowUp!");

}

///////////////////////////////////////

// TForm1 /////////////////////////////


///////////////////////////////////////

__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

}

void __fastcall TForm1::OpenFileBtnClick(TObject *Sender)

{

  AnsiString S;

  TFileObject FileObject("c:\\autoexec.bat");

  
FileObject.OpenFile();

  Memo1->Lines->Add(FileObject.GetData(S));

  FileObject.BlowUp();

}

The reason for concern in the process of opening and closing a stream does not really have much to do with the act of allocating memory for the stream. Nor, in fact, is there much reason to be concerned that something might go wrong when you open the stream:

FMyFile = new TFileStream(FFileName, fmOpenRead);

BCB has all the bases covered in this case, and if you pass in a bogus filename while opening a stream, it will clean things up for you.

The issue in this example, then, is not really memory allocation per se, but only that you want to be sure that the stream is closed up once the process is finished. In other words, something might go wrong while you are reading the stream, or during some other related part of the process. For instance, in a real-world programming example, you might process the data in the stream, with a good chance that an exception will be raised during the processing of the data.

The purpose of the example shown here is to show how to use a destructor to ensure that the memory for the TFileStream object will be deallocated even if something goes wrong while reading from the file. In particular, if something goes wrong during the execution of this block of code:

S.SetLength(FMyFile->Size + 1);

FMyFile->Read(S.c_str(), 
FMyFile->Size);

S[FMyFile->Size] = `\0';

return S;

you can still be sure that the stream will be deallocated:

FMyFile->Free;

The mechanism that ensures that the Free method gets called is nothing more than a simple destructor. This is a common use of the power of destructors in exception handling, and it is one you should be sure you understand.

The preceding code also demonstrates how to open up a simple TFileStream object and read in some data. The newly collected data is then displayed to the user through the auspices of the TMemo object. Once the stream is no longer needed, it is destroyed by a call to Free.

The Create method for a stream takes two parameters. The first is a string specifying the name of the file you want to read. The second is a constant that informs the system what kind of operation you want to perform. The following are the constants associated with BCB streams, as found in the online help:

fmOpenRead Open the file for reading only
fmOpenWrite Open the file for writing only
fmOpenReadWrite Open the file for reading or writing
fmShareExclusive Open the file, disallowing other applications to open it for reading or writing
fmShareDenyWrite Open the file, disallowing other applications to open it for writing
fmShareDenyRead Open the file, disallowing other applications to open it for reading
fmShareDenyNone Open the file, disallowing other applications to open it for exclusive use
fmCreate Create a new file, replacing any existing file with the same name

Streams have several methods associated with them, including generic Read and Write methods, and custom methods that enable you to easily read or write a component to a stream. These latter routines are called ReadComponent and WriteComponent; they will be discussed in some depth in Chapters 19 through 24, which cover objects and components. Streams also have a Size property that reports on the size of the file being read, and a Position property, which identifies your current position in the stream. For further information, see the online help entries for both TFileStream and TStream.

Replacing the Default Exception Handler

You might want to override the default exception handler for a BCB program either because you want to customize your program or because you want to be sure some things happen, regardless of how your program ends. BCB provides an event called OnException in the Application class that can be used for this purpose. A sample program, called OnExcept, demonstrates how to use this event. The code for the program is in Listing 5.10. The form for this program consists of two buttons, one called DivByZero and the other called ReadError.

Listing 5.10. The OnExcept program creates a generic error handler for all exceptions in your program.

#include <vcl.h>

#pragma hdrstop

#include "Main.h"

#pragma resource 
"*.dfm"

TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

  Application->OnException = HandleExcepts;

}

void __fastcall TForm1::ReadErrorClick(

  TObject *Sender)

{

  throw EReadError("Read 
Error");

}

void __fastcall TForm1::DivByZeroClick(

  TObject *Sender)

{

  throw EDivByZero("Exception of type EDivByZero");

}

void __fastcall TForm1::HandleExcepts(TObject *Sender, Exception *E)

{

//  if (typeid(E) = 
typeid(EDivByZero) then

    MessageDlg("Custom OnException: " + E->Message, mtError,

      TMsgDlgButtons() << mbOK, 0);

}

The OnExcept program will handle all exceptions in a single routine defined by the programmer. It is not meant as an example of how to write exceptions or how to construct a program. However, it does provide advanced programmers with an illustration of how to use the OnException event.

The OnException property for TApplication is declared like this:

__property TExceptionEvent OnException =

  {read=FOnException, write=FOnException};

Here is the declaration for TExceptionEvent:

typedef void __fastcall (__closure *TExceptionEvent)

  (System::TObject* Sender, Sysutils::Exception* E);

Normally, the code for an OnXXX handler is created for you automatically when you click the Events page of the Object Inspector. In this case, however, TApplication never appears in the Object Inspector, so you must manually create the call as a method of TForm1:

class TForm1 : public 
TForm

{

__published:

  TButton *DivByZero;

  TButton *ReadError;

  void __fastcall ReadErrorClick(

  TObject *Sender);

  void __fastcall DivByZeroClick(

  TObject *Sender);

private:

  void __fastcall HandleExcepts(TObject *Sender, Exception 
*E);

public:

  virtual __fastcall TForm1(TComponent* Owner);

};

After declaring the function, you can assign it to the TApplication OnException property in the TForm1 constructor:

__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

  Application->OnException = HandleExcepts;

}

The HandleExcepts method will now be called when an exception occurs, as long as you don't declare any intervening try..catch blocks. In the example shown here, the HandleExcepts function explicitly handles EDivByZero errors but responds to all other errors through a generic handler:

void __fastcall TForm1::HandleExcepts(TObject *Sender, Exception *E)

{

//  if (typeid(E) = typeid(EDivByZero) then

    MessageDlg("Custom OnException: " + E->Message,

      mtError, TMsgDlgButtons() << mbOK, 0);


}

Needless to say, it is usually not a good idea to create OnException handlers. You should use them only if you are absolutely sure you know what you are doing and why you want to do it. Otherwise, you can end up destroying the carefully created try..catch block that wraps your programs. The built-in exception-handling capabilities in BCB programs are invaluable aids, and you should be careful not to undermine them.

Using Resources to Track Error Strings

Internationalization issues are discussed briefly in this section, as well as the mechanics of using string tables.

If you look carefully at the Pascal version of the SysUtils unit, you will see that the developers of the VCL rarely hard coded a string into the code that throws exceptions. Instead, the VCL constantly calls functions with names such as LoadStr or FmtLoadStr. These functions call the Windows API routine LoadString, which retrieves a string from a resource:

int LoadString(

    HINSTANCE  hInstance,  // handle of module containing string resource

    UINT  uID,             // resource 
identifier

    LPTSTR  lpBuffer,      // address of buffer for resource

    int  nBufferMax        // size of buffer

   );

The LoadString function is akin to the LoadResource function: Instance: HInstance variable of the current module. This variable is declared and assigned automatically by the system. You don't have to do anything but use the variable, which is passed on to you gratis by both Windows and BCB. (You can load strings from a resource DLL simply by using the DLL's module handle for the Instance parameter to LoadString.)

ID: A number referencing a particular string in a string resource.

Buffer: A buffer to hold the string.

BufferMax
: The maximum size of the string to be placed in the buffer.

Listings 5.11 through 5.14 show how to put this function to work. In a well-designed program, this code will save room in your executables and help when it comes time to internationalize an application. You can see the main form for this program in Figure 5.6.

FIGURE 5.6. The simple main form for the ResError program has four bitmap buttons on it.

Listing 5.11. The ResError program shows how to read error strings from a resource file.

///////////////////////////////////////

// Main.h

// Project: ResError

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef MainH

#define MainH

#include <Classes.hpp>

#include <Controls.hpp>

#include 
<StdCtrls.hpp>

#include <Forms.hpp>

#include <Buttons.hpp>

class ErrorOne;

class ErrorOne: public Exception

{

  typedef Sysutils::Exception inherited;

public:

  __fastcall ErrorOne(const System::AnsiString Msg): 
Exception(Msg) {}

};

class ErrorTwo;

class ErrorTwo: public Exception

{

  typedef Sysutils::Exception inherited;

public:

  __fastcall ErrorTwo(const System::AnsiString Msg): Exception(Msg) {}

  __fastcall ErrorTwo(int Ident, const 
System::TVarRec * Args,

    const int Args_Size): Exception(Ident, Args, Args_Size) {}

};

class TForm1 : public TForm

{

__published:

  TBitBtn *TestLoadStr;

  TBitBtn *RaiseErrorOne;

  TEdit *Edit1;

  TEdit *Edit2;

  TBitBtn *RaiseErrorTwo;

  
TBitBtn *ResourceFormatBtn;

  void __fastcall TestLoadStrClick(

  TObject *Sender);

  void __fastcall RaiseErrorOneClick(

  TObject *Sender);

  void __fastcall RaiseErrorTwoClick(

  TObject *Sender);

  void __fastcall 
ResourceFormatBtnClick(TObject *Sender);

private:

public:

  virtual __fastcall TForm1(TComponent* Owner);

};

extern TForm1 *Form1;

#endif

Listing 5.12. The main module for the ResError program.

///////////////////////////////////////

// Main.cpp

// Project: ResError

// Copyright (c) 1997 by Charlie Calvert

//

#include 
<vcl.h>

#pragma hdrstop

#include "Main.h"

#pragma resource "*.dfm"

#include "resmain.inc"

TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

}

#define Max 150

AnsiString 
GetError(int ID)

{

  AnsiString S;

  S.SetLength(Max);

  LoadString((HINSTANCE)HInstance, ID, S.c_str(), Max);

  return S;

}

void __fastcall TForm1::TestLoadStrClick(TObject *Sender)

{

  Edit1->Text = GetError(ErrorOneStrID);

  
Edit2->Text = GetError(ErrorTwoStrID);

}

void __fastcall TForm1::RaiseErrorOneClick(TObject *Sender)

{

  throw ErrorOne(GetError(ErrorOneStrID));

}

void __fastcall TForm1::RaiseErrorTwoClick(TObject *Sender)

{

  throw 
ErrorTwo(GetError(ErrorTwoStrID));

}

void __fastcall TForm1::ResourceFormatBtnClick(TObject *Sender)

{

  throw ErrorTwo(ResFormatStrID,

    OPENARRAY(TVarRec, (3, "Number too large!")));

}

Listing 5.13. The RESMAIN.INC file lists the errors used by the ResError program.

#define ErrorOneStrID 1

#define ErrorTwoStrID 
2

#define ResFormatStrID 3

Listing 5.14. The RESMAIN.RC file can be converted in a RES file by typing BRCC32 -r RESMAIN.RC at the DOS prompt.

#include "resmain.inc";

STRINGTABLE

{

  ErrorOneStrID, "First error"

  ErrorTwoStrID, "Second error"

  ResFormatStrID, "Custom Error %d: %s \rRaised by ResError Program!"

}

The ResError program loads error strings from a resource and displays them to the user when an exception is thrown. One button on the main form of the program is used to test whether the strings found in the resource can be retrieved, and the other two buttons actually throw exceptions.


NOTE: String tables can help you during development and during maintenance by enabling you to change strings in one place and see the changes reflected throughout your program. Needless to say, this technique can be very useful when you are trying to create multiple versions of a program for internationalization purposes.

When foreign languages are involved, you might store your string resources in a DLL so that you can change the strings without having to recompile your main program. In short, you can create DLLs that contain nothing but string resources. Each DLL will contain strings from a different language so that you have a version that is French, another that is German, and so on. You can then ship a different DLL with the products that you ship to different countries. When using these DLLs, call LoadString, but pass in the HInstance of the DLL where the string is stored. One way to get this HInstance is by calling LoadLibrary explicitly, rather than letting the system call it for you. The value returned by the call to LoadLibrary can be passed to LoadString:

   HINSTANCE LoadLibrary(

    LPCTSTR  lpLibFileName // address of filename of executable module 

   );

Note also that the string resources in your EXE can be removed and replaced without recompiling the EXE. All you have to do is use BRCC32.EXE to tack the new resources onto the precompiled EXE.


To create a string resource, you could use the following syntax in an RC file:

STRINGTABLE

{

  1, "First error"

  2, "Second error"

  3, "Custom Error %d: %s \rRaised by ResError Program!"

}

1, 2, and 3 are the IDs of the strings, and the strings themselves are shown in double quotes.

Notice, however, that ResError does not hard-code numbers into its string table:

#include "resmain.inc";


STRINGTABLE

{

  ErrorOne, "First error"

  ErrorTwo, "Second error"

  ResFormatStrID, "Custom Error %d: %s \rRaised by ResError Program!"

}

Instead, it uses constants that are declared in RESMAIN.INC:

#define ErrorOneStrID 1

#define ErrorTwoStrID 2

#define ResFormatStrID 3

You can, of course, simply add an RC file to a project in order to have it compile automatically. Alternatively, you can compile an RC file into a RES file by typing this line at the DOS prompt:

brcc32 -r resmain.rc

BRCC32.EXE expects your RC script to use #include to reference the include file. Of course, you reference the same include file in a BCB program by writing the following:

#include "resmain.inc"


NOTE: Windows 95 can make life miserable for denizens of the command line. In particular, programs are sometimes installed in deeply nested paths that contain long filenames. For instance, the default location of BCB is installed in the C:\PROGRAM FILES\BORLAND\BCB32 subdirectory. Given this rather bleak scenario, the correct command line for compiling the RES file would be as follows:

c:\progra~1\borland\BCB32\bin\brcc32.exe -r resmain.rc

The issue here, of course, is that the directory PROGRAM FILES is referenced as shown under DOS 7.0, though it may be referenced differently on your system. Note that 4DOS for Windows 95 supports long filenames. Without command-line support for long filenames, the best approach may be for you to create a batch file with the preceding line in it. (See GO.BAT on the CD that ships with this book.)

Or, alternatively, you can install BCB into a different directory. For instance, on my home machine, BCB is in G:\BCB. Besides help with command-line activities, a second great advantage of this system is that it helps you load the include files quickly into the editor without typing a long path each time you need to peruse a few lines of syntax.

I should perhaps add that I do not mean to encourage you to work from the command line. If you have a resource file you want to add to a project, use the IDE to help you manage your resource. It's good to know how to use the command line when necessary, but it's almost always simplest and safest to do things from inside the IDE. The goal of this product is to make C++ a standard again, and the best way to do that is to keep things as simple as possible.


Once you have created a RES file and an include file, you can load the strings into your program with a relatively simple routine:

AnsiString GetError(int ID)

{

  AnsiString S;

  S.SetLength(Max);

  LoadString((HINSTANCE)HInstance, ID, S.c_str(), Max);

  return S;

}

LoadString is part of the Windows API, so it works with NULL-terminated strings. Notice that once again, I avoid hard-coding numbers into the program, and instead declare a constant called Max. Even if you reference a number only once, it's still a good habit to declare it as a constant. The reasoning, of course, is that you can change the number in one place and be sure that all references to that number will change.

The following excerpt from SYSUTILS.RC shows a portion of a string table used by BCB to aid in reporting errors:

#include "sysutils.inc"

STRINGTABLE

{

  SInvalidInteger, "`%s' is not a valid integer value"

  SInvalidFloat, "`%s' is not a valid floating point value"

  
SInvalidDate, "`%s' is not a valid date"

  SInvalidTime, "`%s' is not a valid time"

  SInvalidDateTime, "`%s' is not a valid date and time"

  STimeEncodeError, "Invalid argument to time encode"

  
SDateEncodeError, "Invalid argument to date encode"

  SOutOfMemory, "Out of memory"

  ...

}

Notice that this table obviously relies on Format and related functions to enable the program to insert data into a string before showing it to the user.

The following constants are excerpted from a BCB source file called SYSUTILS.INC and are used as IDs that reference the errors in the string table shown previously:

const

  SInvalidInteger = 65408;

  SInvalidFloat = 65409;

  SInvalidDate = 65410;

  SInvalidTime = 65411;

  SInvalidDateTime = 65412;

  STimeEncodeError = 65413;

  SDateEncodeError = 65414;

  SOutOfMemory = 65415;

  ...


These string constants end up being linked into most of your programs, so you might want to open up SYSUTIL.RC and see if you can use any of them in your own code. The const syntax shown here is the Pascal equivalent of #include.

The ResError program shows an example of using the Format function in an exception:

void __fastcall TForm1::ResourceFormatBtnClick(TObject *Sender)

{

  throw 
ErrorTwo(ResFormatStrID,

    OPENARRAY(TVarRec, (3, "Number too large!")));

}

The parameters passed into this function are formatted with the following string from the program's resource:

"Custom Error %d: %s \rRaised by ResError Program!"

As a result, the user ends up seeing a string that looks like this:

Custom Error 3: Number too large!

Raised by ResError 
Program

The constructor called here internally uses the Format function from the VCL, as shown by the following pseudocode:

AnsiString S = Format("Custom Error %d: %s \rRaised by ResError 
Program!",

  OPENARRAY(TVarRec, (3, "Number too large!")));

Summary

In this chapter you learned about exceptions. Exceptions are a vital part of all well-written programs. However, you should remember that BCB declares a large number of exceptions for handling the errors that occur in your program. As a result, many errors will be handled gracefully without your intervention. For instance, the error popped up in the first EDivByZero example shown in this chapter is perfectly suitable for many programs. That exception was thrown without you having to write special error-handling syntax. Often the best advice when using exceptions is just to step out of the way and let the process work automatically without your intervention.

Exceptions are one of the most important parts of BCB's syntax, and you should dedicate considerable time to learning how they work. Overall, the mechanics of exceptions are fairly simple. However, there is an art to learning exactly when and how to use exceptions. Gaining a skill in this art takes time, and will in most cases evolve over a period of months or years rather than hours or days. However, the basic idea behind exceptions is extremely simple, and given the structure of the VCL it is unlikely they will cause serious problems in your programs. As a result, there is no reason why you can't plunge right in and start using them in your programs immediately. If you find later that you have used them more than necessary, it is a simple matter to remove them.

In this chapter I have concentrated on how exceptions work in BCB. If you want additional information on this interesting topic, you should get a good book on the C++ language. The redoubtable Scott Meyers, in his excellent book More Effective C++, takes a long hard look at the dark side of exceptions. I don't agree with all his conclusions, but if you want a balanced view of the subject, you might use his cautious approach as a counterbalance to my enthusiasm for this subject.

TOCBACKFORWARD


©Copyright, Macmillan Computer Publishing. All rights reserved.