When you develop applications, you have to write both the code that solves a particular problem and the code that checks for errors. Traditionally, the error-handling code is based around the if statement. The if statement is often used to test user input and function results. In simple cases, the if statement may be enough, but in GUI applications, where users have complete freedom, an error may occur anytime and anywhere. Using only the if statement to safeguard the application is not the best idea.
The exception handling mechanism is the best way to catch and respond to errors. When an error occurs in a Delphi application, the application automatically raises an exception. An exception is an object that describes the error that occurred. Raising an exception simply means that the application creates an exception object that describes the error in more detail.
If we don't handle the exception (if we don't write code that catches the exception), the application does it automatically. Usually, the application handles the exception by displaying a message box that notifies the user that an error occurred. For instance, if you pass a string that contains characters that can't be converted to a numerical value or an empty string to the StrToInt function, the function raises an exception (see Figure 13-1).
Figure 13-1: An exception handled by the application
To handle an exception raised by the StrToInt function, we have to put the call to the StrToInt function into a protected block. A protected block is a block that can respond to a certain exception. In Delphi, a protected block looks like this:
try statement(s); except end;
In C++, a protected block looks like this:
try { } catch(...) { }
Statements that might raise an exception are placed in the try block, and any code that handles the exception is placed in the exception handler. The exception handler part of the protected block starts with the reserved word except in Delphi and the reserved word catch in C++.
If you pass a valid string to the StrToInt function and no exceptions occur, only the code in the try block is executed. The code in the exception block is only executed if a statement inside the try block raises an exception.
The following two examples show how to call the StrToInt function and catch any exception that might be raised by it. Listing 13-1A shows how to catch exceptions in a Delphi application, and Listing 13-1B shows how to catch exceptions in a C++Builder application.
Listing 13-1A: Catching an exception in Delphi
procedure TForm1.Button1Click(Sender: TObject); var x: Integer; begin try x := StrToInt(Edit1.Text); except ShowMessage('An error occurred.'); end; end;
Listing 13-1B: Catching an exception in C++
void __fastcall TForm1::Button1Click(TObject *Sender) { int x; try { x = StrToInt(Edit1->Text); } catch(...) { ShowMessage("An error occurred."); } }
Figure 13-2: An exception handled by our exception handler
Now, let's try to create a simple calculator that only enables you to divide two numbers. The user interface of the calculator is displayed in Figure 13-3.
Figure 13-3: The simple calculator
To divide the values entered into the TEdit components we have to write code that first converts both numbers to integers and then divides them. This code can easily raise two exceptions. It can raise the EConvertError exception when a value in one of the TEdit components can't be converted, or it can raise the EDivByZero exception when you try to divide the first number by 0.
Listing 13-2: Dividing two values
procedure TForm1.Button1Click(Sender: TObject); var Num1: Integer; Num2: Integer; begin try Num1 := StrToInt(Edit1.Text); Num2 := StrToInt(Edit2.Text); ShowMessage('Result: ' + IntToStr(Num1 div Num2)); except ShowMessage('You can''t divide these values.'); end; end;
Although you can write exception handlers that catch all exceptions, you should try to handle specific exceptions. The ability to handle a specific exception is provided by the reserved word on. The syntax of the reserved word on is:
on SomeException do HandleTheException;
The on-do construct can only be used inside the exception handler:
try statements(s); except on Exception do HandleException; on AnotherOne do HandleThisOne; end;
If possible, you should use the on-do construct to handle different exceptions in their own way. For instance, you can handle the EConvertError by displaying a message that displays the error, and you can handle the EDivByZero exception by telling the user that the second number can't be 0 and automatically changing it to 1. Listing 13-3A shows how to handle specific exceptions in Delphi. Listing 13-3B shows how to handle specific exceptions in C++.
Listing 13-3A: Handling specific exceptions
procedure TForm1.Button1Click(Sender: TObject); var Num1: Integer; Num2: Integer; begin try Num1 := StrToInt(Edit1.Text); Num2 := StrToInt(Edit2.Text); ShowMessage('Result: ' + IntToStr(Num1 div Num2)); except on EConvertError do ShowMessage('One of the values is invalid.'); on EDivByZero do begin ShowMessage('This value can''t be 0.'); Edit2.Text := '1'; Edit2.SetFocus; end; // on EDivByZero end; end;
When you use the on-do construct to handle specific exceptions, you should also add code that deals with errors you know nothing about. To handle exceptions that you haven't handled specifically, you can add an else part to the exception handler.
try statement(s); except on Exception do HandleIt; on AnotherOne do HandleThisOne; else HandleAllOtherExceptions; end;
To handle specific exceptions in C++, you have to write one or more catch blocks after the try block. When you use catch to catch a specific exception, you can't just specify the exception class as you can in Delphi. In C++, when you catch an exception you also have to specify the exception object and catch it by reference. In Delphi, you can catch an exception with or without catching the exception object. To see how to use the exception object in Delphi, see the "Using the Exception Object" section later in this chapter.
So, to handle specific exceptions in C++, you have to write this:
try { } catch(ExceptionClass &Object) { } catch(AnotherExceptionClass &Object) { } /* if you want to catch other exceptions write catch(...) at the end */ catch(...) { }
Listing 13-3B: Handling specific exceptions in C++
void __fastcall TForm1::Button1Click(TObject *Sender) { int Num1; int Num2; try { Num1 = StrToInt(Edit1->Text); Num2 = StrToInt(Edit2->Text); ShowMessage("Result = " + IntToStr(Num1 / Num2)); } catch(EConvertError &e) { ShowMessage("One of the values is invalid."); } // instead of EDivByZero, division raises an // EAccessViolation exception in a C++Builder application catch(EAccessViolation &e) { ShowMessage("This value can't be 0!"); Edit2->Text = "1"; Edit2->SetFocus(); } }
When an error occurs, an exception object instance is created. When this exception is handled, the exception object is automatically freed. If you don't want to or don't know how to handle a specific exception, you should let Delphi handle it. To do that, you have to reraise the exception, that is, recreate the exception object instance. To reraise the exception in Delphi, you have to use the reserved word raise. In C++, you have to use the reserved word throw.
For instance, the following exception handler only handles the EConvertError exception. If any other exception is raised, the exception handler reraises it. This way the exception remains after the exception handler finishes, and another exception handler, usually the default one, has to handle it. Listing 13-4A shows how to reraise an exception in Delphi and Listing
13-4B shows how to reraise an exception in C++.
Listing 13-4A: Reraising exceptions in Delphi
procedure TForm1.Button1Click(Sender: TObject); var Num1: Integer; Num2: Integer; begin try Num1 := StrToInt(Edit1.Text); Num2 := StrToInt(Edit2.Text); ShowMessage('Result: ' + IntToStr(Num1 div Num2)); except { Handle the EConvertError exception } on EConvertError do ShowMessage('One of the values is invalid.'); { Reraise all other exceptions. } else raise; end; end;
Listing 13-4B: Reraising exceptions in C++
void __fastcall TForm1::Button1Click(TObject *Sender) { int Num1; int Num2; try { Num1 = StrToInt(Edit1->Text); Num2 = StrToInt(Edit2->Text); ShowMessage("Result = " + IntToStr(Num1 / Num2)); } catch(EConvertError &e) { ShowMessage("One of the values is invalid."); } catch(...) { Caption = "Something wrong happened."; // reraise the exception throw; } }
So, if an EConvertError exception is raised, the exception handler handles it itself, and if any other exception occurs, like EDivByZero or EAccess- Violation, the exception handler reraises the exception and passes it to another handler (see Figure 13-4).
Figure 13-4: Local handler handling EConvertError and the default handler handling the reraised exception
The reserved word raise is also used to raise exceptions. To raise an exception in Delphi, use the reserved word raise followed by an exception object instance. The exception object instance is usually a call to the exception constructor.
The syntax for raising an exception typically looks like this:
raise ExceptionClass.Create('Error message');
To raise an exception in C++, use the reserved word throw, also followed by a call to an exception's constructor:
throw ExceptionClass("Error message");
You can, for instance, create a custom version of the StrToInt function that raises the EConvertError exception with customized error messages when the string can't be converted. Listing 13-5A shows the Delphi version of the function and Listing 13-5B shows the C++ version.
Listing 13-5A: Raising exceptions in Delphi
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, StdCtrls; type TForm1 = class(TForm) procedure Button2Click(Sender: TObject); private { Private declarations } public { Public declarations } function CustomStrToInt(const s: string): Integer; end; var Form1: TForm1; implementation {$R *.dfm} function TForm1.CustomStrToInt(const s: string): Integer; var ErrorCode: Integer; begin Val(s, Result, ErrorCode); if ErrorCode <> 0 then begin if s = '' then raise EConvertError.Create('An empty string can''t be used here.') else raise EConvertError.Create('Hello? You can''t convert "' + s + '" to an integer!'); end; end; procedure TForm1.Button2Click(Sender: TObject); var Num1: Integer; Num2: Integer; begin Num1 := CustomStrToInt(Edit1.Text); Num2 := CustomStrToInt(Edit2.Text); ShowMessage(IntToStr(Num1 div Num2)); end;
Figure 13-5: The EConvertError exception raised in the CustomStrToInt function
Listing 13-5B: Raising exceptions in C++
int __fastcall TForm1::CustomStrToInt(const AnsiString s) { int Result; if(s == "0") return 0; else { if(s == "") throw EConvertError("An empty string can't be used here."); else { // atoi returns 0 if it fails to convert the str to int Result = atoi(s.c_str()); if(Result == 0) throw EConvertError("Hello? You can't convert \"" + s + "\" to an integer!"); } return Result; } }
The on-do construct enables us to temporarily acquire the actual exception object by using the following syntax:
on Identifier: Exception do HandleIt;
The identifier is usually the capital letter E. When you acquire the exception object, you can use it like any other object and access its properties and methods. The only thing that you really must not do is destroy the exception object because exception objects are automatically managed by the exception handler.
Listing 13-6: Using the exception object
procedure TForm1.Button1Click(Sender: TObject); var x: Integer; y: Integer; begin x := 20; y := 0; try Caption := IntToStr(x div y); except on E: EDivByZero do ShowMessage('Exception: ' + E.ClassType.ClassName + #13 + 'Exception Message: ' + E.Message); end; end;
Figure 13-6: Using the exception object
Creating a custom exception is very simple and not that different from creating a custom class. Custom exceptions should descend from the Exception class or another descendant of the Exception class. The names of exception classes, by convention, always start with the capital letter E.
type EMyException = class(Exception);
Listing 13-7A shows how to raise and catch a custom exception in Delphi. Listings 13-7B and 13-7C that follow in the next section show how to raise and catch the same custom exception in C++.
Listing 13-7A: Working with a custom exception
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type ENoUpperCaseLetters = class(Exception); TForm1 = class(TForm) Edit1: TEdit; Button1: TButton; Label1: TLabel; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } function CountUpperCase(const s: string): Integer; end; var Form1: TForm1; implementation {$R *.dfm} function TForm1.CountUpperCase(const s: string): Integer; var ch: Char; begin Result := 0; for ch in s do if ch in ['A'..'Z'] then Inc(Result); { if there are no uppercase letters, raise the exception } if Result = 0 then raise ENoUpperCaseLetters.Create('No uppercase letters in the string.'); end; procedure TForm1.Button1Click(Sender: TObject); var Cnt: Integer; begin try Cnt := CountUpperCase(Edit1.Text); Caption := IntToStr(Cnt) + ' uppercase letter'; if Cnt > 1 then Caption := Caption + 's'; except on E: ENoUpperCaseLetters do Caption := E.Message; end; end; end.
Figure 13-7: Working with a custom exception
Creating a custom exception in C++ is only a bit more complicated than it is in Delphi. To create a custom exception, you have to derive a new class from Exception and create a constructor that accepts an AnsiString message and passes it to the Exception's constructor:
class EMyException: public Exception { public: EMyException(const AnsiString Msg) : Exception(Msg) {}; };
The : Exception(Msg) part of the constructor is known as the initializer list. In this case, it simply passes the Msg parameter to the Exception class's constructor.
Listings 13-7B and 13-7C show how to count uppercase letters in C++ and how to raise and catch the ENoUpperCaseLetters custom exception.
Listing 13-7B: Declaring a custom exception in C++
#ifndef Unit1H #define Unit1H #include <Classes.hpp> #include <Controls.hpp> #include <StdCtrls.hpp> #include <Forms.hpp> class ENoUpperCaseLetters: public Exception { public: ENoUpperCaseLetters(const AnsiString Msg): Exception(Msg) {}; }; class TForm1 : public TForm { __published: // IDE-managed Components TButton *Button1; TEdit *Edit1; void __fastcall Button1Click(TObject *Sender); private: // User declarations int __fastcall CountUpperCase(const AnsiString s); public: // User declarations __fastcall TForm1(TComponent* Owner); }; extern PACKAGE TForm1 *Form1; #endif
Listing 13-7C: Working with a custom exception in C++
#include <vcl.h> #pragma hdrstop #include "Unit1.h" #pragma package(smart_init) #pragma resource "*.dfm" TForm1 *Form1; __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } int __fastcall TForm1::CountUpperCase(const AnsiString s) { int cnt = 0; for(int i = 1; i <= s.Length(); i++) { if(s[i] >= 'A' && s[i] <= 'Z') cnt++; } if(cnt == 0) throw ENoUpperCaseLetters("No uppercase letters in the string."); return cnt; } void __fastcall TForm1::Button1Click(TObject *Sender) { try { int Cnt = CountUpperCase(Edit1->Text); Caption = IntToStr(Cnt) + " uppercase letter"; if(Cnt > 1) Caption = Caption + "s"; } catch(ENoUpperCaseLetters &e) { Caption = e.Message; } }