C++Builder 5 Developers Guide

   

Better Programming Practices in C++Builder

This section looks at some ways to improve how you write C++ code in C++Builder. Entire books are devoted to better C++ programming, and you are encouraged to read such texts to deepen your understanding of C++. The topics discussed here are those that have particular relevance to C++Builder, and those that are often misunderstood or misused by those new to C++Builder.

Use a String Class Instead of char*

Say goodbye to char* for string manipulation. Use either the string class provided by the C++ Standard Library or the VCL's native string class AnsiString (which has been conveniently typedef ed to String ). You can even use both. Access the C++ Standard Library's string class by including the statement

#include <string>

at the top of your code. If portability across operating systems is a goal, you cannot use the VCL, so this is the string class to use. Otherwise , use AnsiString , which has the advantage that it is the string representation used throughout the VCL and, thus, allows your code to work seamlessly with the VCL. For circumstances in which an old-style char* string is required, such as to pass a parameter to a Win32 API call, both string classes offer the c_str() member function, which returns such a string. In addition, the AnsiString class also offers the popular old-style sprintf() and printf() functions (for concatenating strings) as member functions. It offers two varieties of each: a standard version and a cat_ version. The versions differ in that the cat_ version adds the concatenated string to the existing AnsiString , and the standard version replaces any existing contents of the AnsiString . The difference between the sprintf() and printf() member functions is that sprintf() returns a reference to the AnsiString , and printf() returns the length of the final formatted string (or the length of the appended string, in the case of cat_printf ). The function declarations are

int __cdecl printf(const char* format, ...); int __cdecl cat_printf(const char* format, ...); AnsiString& __cdecl sprintf(const char* format, ...); AnsiString& __cdecl cat_sprintf(const char* format, ...);

These member functions ultimately call vprintf () and cat_ vprintf() in their implementation. These member functions take a va_list as their second parameter as opposed to a variable argument list. This requires the addition of the #include <stdarg.h> statement in your code. The function declarations are

int __cdecl vprintf(const char* format, va_list paramList); int __cdecl cat_vprintf(const char* format, va_list paramList);

The respective printf() and sprintf() functions perform the same task, differing only in their return types. As a result, this is the only criterion that is required when deciding which of the two to use.

WARNING

Note that the printf() and sprintf() AnsiString member functions in C++Builder version 4 are the same as the cat_printf() and cat_sprintf() functions in version 5, not the printf() and sprintf() AnsiString member functions. Care should be taken when converting code between those two versions.

Understand References and Use Them Where Appropriate

References are often misunderstood and, therefore, are not used as often as they should be. Often, it is possible to replace pointers with references, making the code more intuitive and easier to maintain. This section looks at some of the features of references and when they are most appropriately used.

The reason for the abundance of pointer parameters in the VCL in C++Builder is often a key point of controversy among C++ programmers moving to C++Builder.

A reference always refers to only one object, its referent , and it cannot be rebound to refer to a different object (object in this context includes all types). A reference must be initialized on creation; a reference cannot refer to nothing ( NULL ). Pointers, on the other hand, can point to nothing ( NULL ), can be re-bound, and do not require initialization on creation. A reference should be considered an alternative name for an object, whereas a pointer should be considered an object in itself. Anything that is done to a reference is also done to its referent and vice versa. This is because a reference is just an alternative name for the referent; they are the same. You can see, therefore, that references, unlike pointers, are implicitly dereferenced.

The following code shows how a reference can be declared:

int X = 12; // Declare and initialize int X to 12 int& Y = X; // Declare a reference to an int, i.e. Y, and // initialize it to refer to X

If you change the value of Y or X , you also change the value of X or Y , respectively, because X and Y are two names for the same thing. Another example of declaring a reference to a dynamically allocated variable is

TBook* Book1 = new TBook(); // Declare and create a TBook object TBook& Book2 = *Book1; // Declare a TBook reference, // i.e. Book2, and initialize it // to refer to the object pointed // by Book1 The object pointed to by Book1 is the referent of the reference Book2.

One of the most important uses for references is the passing of user -defined types as parameters to functions. A parameter to a function can be passed by reference by making the parameter a reference and calling the function as if it were passed by value. For example, the following function is the typical swap function for two int s:

void swap(int& X, int& Y) { int temp; temp = X; X = Y; Y = temp; }

This function would be called as follows :

int Number1 = 12; int Number2 = 68; Swap(Number1, Number2); // Number1 == 68 and Number2 == 12

Number1 and Number2 are passed by reference to swap , and, therefore, X and Y become alternative names for Number1 and Number2 , respectively, within the function. What happens to X also happens to Number1 and what happens to Y also happens to Number2 . A predefined type such as an int should be passed by reference only when the purpose is to change its value; otherwise, it is generally more efficient to pass by value. The same cannot be said for user-defined types (classes, struct s, and so on). Rather than pass such types to functions by value, it is more efficient to pass such types by const reference or, if the type is to be changed, by non- const reference or pointer. However,

void DisplayMessage(const AnsiString& message) { //Display message. // message is an alias for the AnsiString argument passed // to the function. No copy is made and the const qualifier // states that the function will not (cannot) modify message }

is not really better than:

void DisplayMessage(AnsiString message) { //Display message. // message is a copy of the AnsiString argument passed }

This is because AnsiString itself implements a copy-on-write , shared string model, and thus only a very small object is passed from caller to function.

But for non-VCL classes (because VCL classes other than AnsiString are usually passed as const pointers) the first format is better for two reasons. First, the parameter is passed by reference. This means that when the function is called, the object used as the calling argument is used directly, rather than being copied . The copy constructor of AnsiString does not need to be invoked (as it would be on entering the second function), and neither does the destructor, as it would be at the end of the second function when message goes out of scope. Second, the const keyword is used in the first function to signify that the function will not modify message through message . Both functions are called in the same way:

AnsiString Message = "Hello!"; DisplayMessage(Message);

Functions can also return references, which has the side effect of the function becoming an c (a value that can appear on the left side of an expression) for the referent. This also enables operators to be written that appear on the left side of an expression, such as the subscript operator. For example, given the Book class, an ArrayOfBooks class can be defined as follows:

class Book { public: Book(); int NumberOfPages; }; class ArrayOfBooks { private: static const unsigned NumberOfBooks = 100; public: Book& operator[] (unsigned i); };

In this case, an instance of ArrayOfBooks can be used just like a normal array. Elements accessed using the subscript operator can be assigned to and read from, similar in the following:

ArrayOfBooks ShelfOfBooks; unsigned PageCount = 0; ShelfOfBooks[0].NumberOfPages = 45; // A short book! PageCount += ShelfOfBooks[0].NumberOfPages; //PageCount = 45

This is possible because the value returned by the operator is the actual referent, not a copy of the referent.

Generally, you can say that references are preferred to pointers because they are safer (they can't be re-bound and don't require testing for NULL because they must refer to something). Also, they don't require explicit dereferencing, making code more intuitive.

But what about the pointers used in C++Builder's VCL?

The extensive use of pointers in the VCL is caused by the fact that the VCL is written in, and must remain compatible with Delphi (Delphi), which uses Delphi-style references. A Delphi-style reference is closer to a C++ pointer than a C++ reference. This has the side effect that, when the VCL is used with C++, pointers have to be used as replacements for Delphi references. This is because a Delphi reference (unlike a C++ reference) can be set to NULL and can be re-bound. In some cases it is possible to use reference parameters instead of pointer parameters, but because all VCL-based objects are dynamically allocated on free store and, therefore, are referred to through pointers, the pointers must be dereferenced first. Because the VCL relies on some of the features of Delphi references, pointers are used for object parameter passing and returning. Remember that a pointer parameter is passed by value, so the passed pointer will not be affected by the function. You can prevent modification of the object pointed to by using the const modifier.

Avoid Using Global Variables

Unless it is absolutely necessary, don't use global variables in your code. Apart from polluting the global namespace (and increasing the chance of a name collision), it increases the dependencies between translation units that use the variables. This makes code difficult to maintain and minimizes the ease with which translation units can be used in other programs. The fact that variables are declared elsewhere also makes code difficult to understand.

One of the first things any astute C++Builder programmer will notice is the global form pointers present in every form unit. This might give the impression that using global variables is OK; after all, C++Builder does it. However, C++Builder does this for a reason, which we will discuss at the end of this section. For now, we will examine some of the alternatives to declaring global variables.

Let's assume that global variables are a must. How can we use global variables without incurring some of the side effects that they produce? The answer is that we use something that acts like a global variable, but is not one. We use a class with a member function that returns a value of, or reference to (whichever is appropriate), a static variable that represents our global variable. Depending on the purpose of our global variables (for example, global to a program or global to a library), we may or may not need access to the variables through static member functions. In other words, it might be possible to instantiate an object of the class that contains the static variables when they are required. We consider first the case where we do require access to the static variables (representing our global variables) through static member functions. We commonly refer to this kind of class as a module .

With a module of global variables, you improve your representation of the variables by placing them into a class, making them private static variables, and using static getters and setters to access them (for more information, see Large-Scale C++ Software Design by Lakos, 1996, p. 69). This prevents pollution of the global namespace and gives a certain degree of control over how the global variables are accessed. Typically, the class would be named Global . Hence, two global variables declared as

int Number; double Average;

could be replaced by

class Global { private: static int Number; static double Average; //PRIVATE CONSTRUCTOR Global(); //not implemented, instantiation not possible public: // SETTERS static void setNumber(int NewNumber) { Number = NewNumber; } static void setAverage(double NewAverage) { Average = NewAverage; } // GETTERS static int getNumber() { return Number; } static double getAverage() { return Average; } };

Accessing Number is now done through Global:: getNumber() and Global:: setNumber() . Average is accessed similarly. The class Global is effectively a module that can be accessed throughout the program and does not need to be instantiated (because the member data and functions are static).

Often such an implementation is not required, and it is possible to create a class with a global point of access that is constructed only when first accessed. This has the benefit of allowing control over the order of initialization of the variables (objects must be constructed before first use). The method used is to place the required variables inside a class that cannot be directly instantiated, but accessed only through a static member function that returns a reference to the class. This ensures that the class containing the variables is constructed on first use and is constructed only once.

This approach is often referred to as the Singleton pattern (for more information, see Design Patterns: Elements of Reusable Object-Oriented Software by Gamma et al ., 1995, p. 127). Patterns are a way of representing recurring problems and their solutions in object-based programs. For more on patterns, see Chapter 4, " Creating Custom Components."

The basic code required to create a Singleton (as such a class is commonly referred to) is as follows:

class Singleton { public: static Singleton& Instance(); protected: Singleton(); // Not Implemented, Instantiation not possible };

An implementation of Instance is

Singleton& Singleton::Instance() { static Singleton* NewSingleton = new Singleton(); return *NewSingleton; }

The initial call to Instance will create a new Singleton and return a reference to it. Subsequent calls will simply return a reference. However, the destructor of the Singleton will not be called; the object is simply abandoned on free store. If there is important processing that must be executed in the destructor, the following implementation will ensure that the Singleton is destructed:

Singleton& Singleton::Instance() { static Singleton NewSingleton; return NewSingleton; }

This implementation causes its own problem. It is possible for another static object to access the Singleton after it has been destroyed . One solution to this problem is the nifty counter technique (for more information, see C++ FAQs Second Edition , Cline et al ., 1999, p. 235, and Large-Scale C++ Software Design , Lakos, 1996, p. 537), in which a static counter is used to control when each object is created and destroyed. If you find the need for this technique, perhaps a rethink of the code would also be helpful. It might be that a slight redesign could remove the dependency.

It should now be clear that static variables are like global variables and can almost always be used in place of global variables. Remember, though, that ultimately global variables should be avoided.

Understand How C++Builder Uses Global Variables

What then of the global form pointer variables in C++Builder? Essentially , global form pointer variables are present to enable the use of nonmodal forms. Nonmodal forms are conventional windows that last for long periods of time and enable you to work with the rest of the application, even while they are open . Modal forms are dialogs, which block interaction with the rest of the application.

Nonmodal forms require a global point of access for as long as the form exists, and it is convenient for the IDE to automatically create one when the form is made. The default operation of the IDE is to add newly created forms to the autocreate list, which adds the line

Application->CreateForm(__classid(TFormX), &Form X );

(where X is a number) to the WinMain function in the project .cpp file. Modal forms do not require this because the ShowModal() method returns after the forms are closed, making it possible to delete them in the same scope as they were created. General guidelines on the use of forms can, therefore, be given.

TIP

You can uncheck the Auto Create Forms option on the forms property page in the Tools, Environment Options menu to change the behavior of the IDE so that forms are not automatically added to the autocreate list. When this is done, forms are instead added to the available list.

First, determine if a form is to be a modal form or a nonmodal form.

If the form is modal, it is possible to create and destroy the form in the same scope. This being the case, the global form pointer variable is not required, and the form should not be autocreated. Remove the Application->CreateForm entry from WinMain either by deleting it or by removing it from the autocreate list on the forms page in the Project, Options menu. Next , either delete or comment out the form pointer variable from the .h and .cpp files, and state explicitly in the header file that the form is modal and should be used only with the ShowModal() method. That is, in the .cpp file remove

TFormX* FormX;

and from the .h file, remove

extern PACKAGE TFormX* FormX;

Add a comment such as the following:

// This form is MODAL and should only called with the ShowModal() method.

To use the form, simply write

TFormX* FormX = new TFormX(0); try { FormX->ShowModal(); } __finally { delete FormX; }

Because you most likely do not want the form pointer to point elsewhere, you could declare the pointer as const :

TFormX* const FormX = new TFormX(0); try { FormX->ShowModal(); } __finally { delete FormX; } TFormX(this); FormX->ShowModal(); delete FormX;

The use of a try / __finally block ensures that the code is exception-safe. An alternative to these examples is to use the Standard Library's auto_ptr class template:

auto_ptr<TFormX> FormX(new TFormX(0)); FormX->ShowModal();

NOTE

You might need to reference the std namespace to create an auto_ptr ; this is done either by using a namespace std or by prefixing auto_ptr with std:: .

Whichever technique you use, you are guaranteed that if the code terminates prematurely because an exception is thrown, FormX will be automatically destroyed. With the first technique this happens in the __finally block; with the second it occurs when auto_ptr goes out of scope. The second technique can be further enhanced by making the auto_ptr const because generally it is not required that the auto_ptr lose ownership of the pointer, as in the following code. (For more information, see Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions by Sutter, 2000, p. 158.)

const auto_ptr<TFormX> FormX(new TFormX(0)); FormX->ShowModal();

Of particular note in the code snippets is that 0 ( NULL ) is passed as the argument to the AOwner parameter of FormX . This is because we handle the destruction of the form ourselves .

TIP

Using auto_ptr is an effective way of managing the memory of VCL-based objects. It is exception-safe and easy to use. For a VCL object that takes an owner parameter in its constructor, you can simply pass because you know that the object will be deleted when the auto_ptr goes out of scope.

If the form is non-modal, you must only decide whether you want it autocreated. If you don't, you must ensure that it is removed from WinMain . When you want it created later, you can use the form's global pointer and the new operator. Show the form using the Show() method. Remember that you cannot delete modal forms because Show() returns when the form is shown, not when it is closed. Therefore, it might still be in use. For example, if the form is autocreated, write

FormX->Show();

Otherwise, create and show it this way:

FormX = new TFormX(this); FormX->Show();

As an aside to this topic, the practice of declaring variables or functions as static so that they have scope only within the translation unit in which they are declared is deprecated. Instead, such variables and functions should be placed in an unnamed namespace. (For more information, see ANSI/ISO C++ Professional Programmer's Handbook: The Complete Language by Kalev, 1999, p. 157.)

Understand and Use const in Your Code

The const keyword should be used as a matter of course, not as an optional extra. Declaring a variable const enables attempted changes to the variable to be detected at compile time (resulting in an error) and also indicates the programmer's intention not to modify the given variable. Moreover, not using the const keyword indicates the programmer's intention to modify a given variable. The const keyword can be used in a variety of ways.

First, it can be used to declare a variable as a constant:

const double PI = 3.141592654;

This is the C++ way to declare constant variables. Do not use #define statements. Note that const variables must be initialized. The following shows the possible permutations for declaring const variables. Pointer and reference declarations are read from right to left, as the following examples show:

int Y = 12; const int X = Y; // X equals Y which equals 12, therefore X = 12 // X cannot be changed, but Y can // In the next declaration the pointer itself is constant int* const P = &Y; // The int pointed to by P, i.e. Y can be // changed through P but P itself cannot change // The next two declarations are the same: const int* P = &Y; // The int pointed to by P, i.e. int const* P = &Y; // Y cannot be changed through P // The next two declarations are the same: const int* const P = &Y; // Neither P, nor what it points to, P int const* const P = &Y; // i.e. Y can be changed through P // The next two declarations are the same: const int& R = Y // The int referred to by R, i.e. R int const& R = Y // Y cannot be changed through R

After reviewing the previous examples, it is helpful to reiterate how const is used with pointer declarations. As stated previously, a pointer declaration is read from right to left so that in int * const the const refers to the * . Hence, the pointer is constant, but the int it points to can be changed. With int const * the const refers to the int . In this case, the int itself is constant, though the pointer to it is not. Finally, with int const * const , both the int and the * are constant. Also remember that int const and const int are the same, so const int * const is the same as int cosnt * const .

If you want to declare a literal string of char s, declare it as one of the following:

const char* const LiteralString = "Hello World"; char const * const LiteralString = "Hello World";

Both of the previous strings and the pointers to them are constant.

Function parameters should be declared as const in this fashion when it is appropriate, such as when the intention of the function is not to modify the argument that is passed to the function. For example, the following function states that it will not modify the arguments passed to it:

double GetAverage(const double* ArrayOfDouble, int LengthOfArray) { double Sum = 0; for(int i=0; i<LengthOfArray; ++i) { Sum += ArrayOfDouble[i]; } double Average = Sum/LengthOfArray; return Average; }

Another way of thinking about this is to assume that if the const keyword is not used for a parameter, it must be the intention of the function to modify that parameter's argument, unless the parameter is pass- by-value (a copy of the parameter is used, not the parameter itself). Notice that declaring int LengthOfArray as a const is inappropriate because this is pass-by-value. LengthOfArray is a copy, and declaring it as a const has no effect on the argument passed to the function. Similarly, ArrayOfDouble is declared as follows:

const double* ArrayOfDouble

not

const double* const ArrayOfDouble

Because the pointer itself is a copy, only the data that it points to needs to be made const .

The return type of a function can also be const . Generally, it is not appropriate to declare types returned by value as const , except in the case of requiring the call of a const -overloaded member function. Reference and pointer return types are suitable for returning as const s.

Member functions can be declared const . A const member function is one that does not modify the this object ( *this ). Hence, it can call other member functions inside its function body only if they are also const . To declare a member function const , place the const keyword at the end of the function declaration and in the function definition at the same place. Generally, all getter member functions should be const because they do not modify *this . For example

class Book { private: int NumberOfPages; public: Book(); int GetNumberOfPages() const; };

The definition of GetNumberOfPages( ) could be

int Book::GetNumberOfPages() const { return NumberOfPages; }

The final area in which const is commonly encountered is when operators are overloaded by a class and access to both const and non- const variables is required. For example, if a class ArrayOfBooks is created to contain Book objects, it is sensible to assume that the [] operator will be overloaded (so that the class acts like an array). However, the question of whether the [] operator will be used with const or non- const objects must be considered. The solution is to const -overload the operator, as the following code indicates:

class ArrayOfBooks { public: Book& operator[] (unsigned i); const Book& operator[] (unsigned i) const; };

The ArrayOfBooks class can use the [] operator on both const and non- const Book s. For example, if an ArrayOfBooks object is passed to a function by reference to const , it would be illegal for the array to be assigned to using the [] operator. This is because the value indexed by i would be a const reference, and the const state of the passed array would be preserved.

Remember, know what const is and use it whenever you can.

Be Familiar with the Principles of Exceptions

Exceptions offer a mechanism for handling runtime errors in a program. Several approaches can be taken to handling runtime errors, such as returning error codes, setting global error flags, and exiting the program. But, in most circumstances, an exception is the only appropriate method. (For more information, see ANSI/ISO C++ Professional Programmer's Handbook: The Complete Language by Kalev, 1999, p. 113.)

Exceptions will commonly be encountered in two forms in C++Builder programs: C++ exceptions and VCL exceptions. Generally, the principles involved with both are the same, but there are some differences.

C++ uses three keywords to support exceptions: try , catch , and throw . C++Builder extends its exception support to include the __finally keyword.

The try , catch , and __finally keywords are used as headers to blocks of code (that is, code that is enclosed between braces). Also, for every try block there must always be one or more catch blocks or a single __finally block.

The try Keyword

The try keyword is used in one of two possible ways. The first and simplest is as a simple block header, to create a try block within a function. The second is as a function block header, to create a function try block, either by placing the try keyword in front of the function's first opening brace or, in the case of constructors, in front of the colon that signifies the start of the initializer list.

NOTE

C++Builder does not currently support function try blocks. However, because it makes a real difference only with constructors, and even then has little impact on their use, it is unlikely that its omission will have any effect. For those who are interested, it will be supported in version 6 of the compiler.

The catch Keyword

Normally, at least one catch block will immediately follow any try block (or function try block). A catch block will always appear as the catch keyword followed by parentheses containing a single exception type specification with an optional variable name. Such a catch block (commonly referred to as an exception handler ) can catch only an exception whose type exactly matches the exception type specified by the catch block. However, a catch block can be specified to catch all exceptions by using the catch all ellipses exception type specifier , catch(...) .

A typical try / catch scenario is as follows:

try { // Code that may throw an exception } catch(exception1& e) { // Handler code for exception1 type exceptions } catch(exception2& e) { // Handler code for exception2 type exceptions } catch(...) { // Handler code for any exception not already caught }

The __finally Keyword

The last of these, __finally , has been added to allow the possibility of performing cleanup operations or ensuring certain code is executed regardless of whether an exception is thrown. This works because code placed inside a __finally block will always execute, even when an exception is thrown in the corresponding try block. This allows code to be written that is exception-safe and will work properly in the presence of exceptions. A typical try / __finally scenario is

try { // Code that may throw an exception } __finally { // Code here is always executed, even if // an exception is thrown in the preceding // try block }

It should be noted that try / catch and try / __finally constructs can be nested inside other try / catch and try / __finally constructs.

The throw Keyword

The throw keyword is used in one of two ways. The first is to throw (or rethrow) an exception, and the second is to allow the specification of the type of exceptions that a function might throw. In the first case (to throw or rethrow an exception), the throw keyword is followed optionally by parentheses containing a single exception variable (often an object) or simply the single exception variable after a space, similar to a return statement. When no such exception variable is used, the throw keyword stands on its own. Then, its behavior depends on its placement. When placed inside a catch block, the throw statement rethrows the exception currently being handled. When placed elsewhere, such as when there is no exception to rethrow, it causes terminate() to be called, ultimately ending the program. It is not possible to use throw to rethrow an exception in VCL code. The second use of the throw keyword is to allow the specification of the exceptions that a function might throw. The syntax for the keyword is

throw( <exception_type_list> )

The exception_type_list is optional and, when excluded, indicates that the function will not throw any exceptions. When included, it takes the form of one or more exception types separated by commas. The exception types listed are the only exceptions the function can throw.

Unhandled and Unexpected Exceptions

In addition to the three keywords described, C++ offers mechanisms to deal with thrown exceptions that are not handled by the program and exceptions that are thrown, but are not expected. This might include an exception that is thrown inside a function with an incompatible exception specification.

When an exception is thrown, but not handled, terminate() is called. This calls the default terminate handler function, which by default calls abort() . This default behavior should be avoided because abort() does not ensure that local object destructors are called. To prevent terminate() being called as a result of an uncaught exception, the entire program can be wrapped inside a try / catch(...) block in WinMain() (or main() for command-line programs). This ensures that any exception will eventually be caught. If terminate() is called, you can modify its default behavior by specifying your own terminate handler function. Simply pass the name of your terminate handler function as an argument to the std:: set_terminate() function. The <stdexcept> header file must be included. For example, given a function declared as

void TerminateHandler();

The code required to ensure that this handler is called in place of the basic termi nate() handler is

#include <stdexcept> std::set_terminate(TerminateHandler);

When an exception is thrown that is not expected, unexpected() is called. Its default behavior is to call terminate() . Again, the opportunity exists to define your own function to handle this occurrence. To do so, call std:: set_unexpected() , passing the function handler name as an argument. The <stdexcept> header file must be included.

Using Exceptions

This brings the discussion to consideration of the exceptions that can and should be thrown by a function and where such exceptions should be caught. This should be decided when you are designing your code, not after it has already been written. To this end, you must consider several things when you write a piece of code. Some of the topics are very complex, and it is beyond the scope of this book to cover all the issues involved.

You must consider if the code you have written could throw one or more exceptions. If so, you must then consider if it is appropriate to catch one or more of the exceptions in the current scope or let one or more of them propagate to an exception handler outside the current scope. If you do not want one or more of the exceptions to propagate outside the current scope, you must place the code in a try block and follow it with the one or more appropriate catch blocks to catch any desired exceptions (or all exceptions, using a catch-all block). To this end, you should be aware of the exceptions built into the language itself, the C++ Standard Library, and the VCL, and be aware of when they can be thrown. For example, if new fails to allocate enough memory, std::bad_alloc is thrown.

Throw an exception in a function only when it is appropriate to do so, when the function cannot meet its promise. (For more information, see C++ FAQs , Second Edition, Cline et al ., 1999, p. 137.)

You should catch an exception only when you know what to do with it, and you should always catch an exception by reference. (For more information, see More Effective C++: 35 New Ways to Improve Your Programs and Designs by Meyers, 1996, p. 68.) VCL exceptions cannot be caught by value. Also, it might not be possible to fully recover from an exception, in which case, the handler should perform any possible cleanup, and then rethrow the exception.

You should understand when and how to use exception specifications for functions and be wary of the possibility of writing an incorrect specification. This will result in unexpected() being called if an unspecified exception is thrown inside a function and it is not handled within that function.

You should ensure that you write exception-safe code that works properly in the presence of exceptions. For example, simple code such as this is not exception safe:

TFormX* const FormX = new TFormX(0); FormX->ShowModal(); delete FormX;

If an exception is thrown between the creation and deletion of the form, it will never be deleted, so the code does not work properly in the presence of exceptions. For an exception-safe alternative, see the section "Avoid Using Global Variables," earlier in this chapter.

If you are writing library or container classes, endeavor to write code that is exception-neutral ”code that propagates all exceptions to the caller of the function that contains the code. (For more information, see Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions by Sutter, 2000, p. 25.)

Never throw an exception from a destructor because the destructor might have been called as a result of stack unwinding after a previous exception was called. This calls terminate() . Destructors should have an exception specification of throw() .

A Final Note on Exceptions

Finally, you should appreciate the differences between VCL and C++ exceptions. VCL exceptions allow operating system exceptions to be handled as well as exceptions generated from within the program. Such exceptions must be caught by reference. VCL exceptions generated from within the program cannot be caught by value. An advantage of VCL exceptions is that they can be thrown and caught within the IDE.

Use new and delete to Manage Memory

The VCL requires that all classes that inherit from TObject be created dynamically from free store. Free store is often referred to as the heap, but free store is the correct term when applied to memory allocated and deallocated by new and delete . The term heap should be reserved for the memory allocated and deallocated by malloc() and free() . (For more information, see Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions by Sutter, 2000, p. 142.) This means a lot of calls to new and delete in C++Builder programs, so it is important to understand a few things about how new and delete work.

WARNING

A Non-Plain Old Data (Non-POD) object is essentially all but the most trivial of classes. Such objects must have their memory allocated by using new ; the C equivalent malloc() will not suffice (its behavior is undefined) and be subsequently deallocated with delete , not free() . The new and delete operators ensure that, in addition to the allocation/deallocation of memory, the object's constructor and destructor, respectively, are called.

The new operator also returns a pointer that is suitable to the object created; not merely a void pointer that must be cast to the required type. new and delete call operator new / operator delete , respectively, to allocate/deallocate memory, and these can be overloaded for specific classes. This enables the customization of memory allocation/deallocation behavior. This is not possible with malloc() and free() .

(For more information, see ANSI/ISO C++ Professional Programmer's Handbook: The Complete Language by Kalev, 1999, p. 221.)

A successful call to new allocates sufficient memory in free store (using operator new ) calls the object's constructor and returns a pointer of the type pointer-to-the-object-type-created. A correctly initialized object is the result. Subsequently, calling delete calls the object's destructor and deallocates the memory obtained previously by calling new .

WARNING

Never call a VCL object's Free() method to destroy a VCL object. Always use delete . This ensures that the object's destructor is called and that the memory allocated previously with new is freed. Free() does not guarantee this, and it is bad practice to use it.

If the call to new is unsuccessful , a std::bad_alloc exception is thrown. Note that the bad_alloc exception is defined in the standard library file <new> . Hence, you must include #include <new> in your program, and it is in the std namespace. It does not return NULL . Therefore, you should not check the return pointer for equality with NULL . The program should be prepared to catch the std::bad_alloc exception and, if the function that calls new does not catch the exception, it should pass the exception outside the function so that calling code has the opportunity to catch it. Either of the following would be appropriate:

void CreateObject(TMyObject* MyObject) throw() { try { MyObject = new TMyObject(); } catch(std::bad_alloc) { //Print a message "Not enough memory for MyObject"; // Deal with the problem // or exit gracefully } } or

void CreateObject(TMyObject* MyObject) throw(std::bad_alloc) { MyObject = new TMyObject(); }

The use of exceptions enables the code that handles the error to be centralized, which leads to safer code that is more intuitive. The throw keyword added to the function header is called an exception specification . The effect of its inclusion in the function header is to specify which exceptions the function can throw. For more explanation refer to the section "Be Familiar with the Principles of Exceptions," earlier in this chapter. In the case of the first CreateObject() function, a throw() exception specifier is used to indicate that no exception will be thrown by the function. This is acceptable because the only exception that can be thrown, std::bad_alloc , is caught and dealt with by the function itself. In the case of the second implementation of CreateObject() , the exception specifier throw(std::bad_alloc) is used to indicate that the only exception that the function can throw is std::bad_alloc . This should be caught and handled by one of the calling routines.

There is also the possibility of writing your own out-of-memory function handler to deal with failed memory allocation. To set a function as a handler for out-of-memory conditions when using new , call the set_new_handler() function (also defined in <new> ), passing as a parameter the name of the function you will use as the out-of-memory handler. For example, if you write a function (nonmember or static member) called OutOfMemory to handle such occurrences, the necessary code is

#include <new> void OutOfMemory() { // Try to free some memory // if there is now enough memory then this // function will NOT be called next time // else either install a new handler or throw an exception } // Somewhere in the main code, near the start write: std::set_new_handler(OutOfMemory);

This code requires some explanation, because the sequence of events that occurs when new fails dictates how the OutOfMemory function should be written. If new fails to allocate enough memory, OutOfMemory is called. OutOfMemory tries to free some memory (how this is done will be discussed later); new will then try again to allocate the required memory. If it is successful, you are finished. If it is unsuccessful, the process just described will be repeated. In fact, it will repeat infinitely until either enough memory is allocated or the OutOfMemory function terminates the process.

To terminate the process, the OutOfMemory function can do several things. It can throw an exception (such as std::bad_alloc() ), it can install a different memory handler that can then try to make more memory available, it can assign NULL to set_new_handler ( std::set_new_handler(0) ), or it can exit the program (not recommended). If a new handler is installed, then this series of events will occur for the new handler (which is called on the subsequent failed attempt). If the handler is set to NULL (0), then no handler will be called, and the exception std::bad_alloc() will be thrown.

Making more memory available is dependent on the design of the program and where the memory shortage arises. If the program keeps a lot of memory tied up for performance reasons but does not always require it to be available at all times, then such memory can be freed if a shortage occurs. Identifying such memory is the difficult part. If there is no such memory usage in the program, the shortage will be a result of factors external to the program such as other memory-intensive software or physical limitations. There is nothing that can be done about physical limitations, but it is possible to warn the user of a memory shortage so that memory- intensive software can be shut down, thereby freeing additional memory.

The trick is to give an advance warning before all the memory is used. One approach is to preallocate a quantity of memory at the beginning of the program. If new fails to allocate enough memory, the memory successfully allocated can be freed. The user is warned that memory is low and told to try to free more memory for the application. Assuming that the preallocated block was large enough, the program should be able to continue operating as normal if the user has freed additional memory. This preemptive approach is simple to implement and reasonably effective.

It is important to note that if you want to allocate raw memory only, operator new and operator delete should be used instead of the new and delete operators. (For more information, see More Effective C++: 35 New Ways to Improve Your Programs and Designs by Meyers, 1996, p. 38.) This is useful for situations in which, for example, a structure needs to be allocated dynamically, and the size of the structure is determined through a function call before the dynamic allocation. This is a common occurrence in Win32 API programming:

DWORD StructureSize = APIFunctionToGetSize(SomeParameter); WIN32STRUCTURE* PointerToStructure; PointerToStructure = static_cast<WIN32STRUCTURE*>(operator new(StructureSize)); // Do something with the structure operator delete(PointerToStructure);

It is clear that the use of malloc() and free() should not be required.

Finally, we will discuss the use of new and delete in dynamically allocating and deallocating arrays. Arrays are allocated and deallocated using operator new[] and operator delete[] , respectively. They are separate operators from operator new and operator delete . When new is used to create an array of objects, it first allocates the memory for the objects (using operator new[] ), and then calls its default constructor to initialize each object. Deleting an array with delete performs the opposite task: It calls the destructor for each object, and then deallocates the memory (using operator delete[] ) for the array. So delete knows to call operator delete[] instead of operator delete , a [] is placed between the delete keyword and the pointer to the array to be deleted:

delete [] SomeArray;

Allocating a single-dimensional array is straightforward. The following format is used:

TBook* ArrayOfBooks = new TBook[NumberOfBooks];

Deleting such an array is also straightforward. However, remember that the correct form of delete must be used ” delete [] . For example

delete [] ArrayOfBooks;

Remember that [] tells the compiler that the pointer is to an array, as opposed to simply a pointer to a single element of a given type. If an array is to be deleted, it is essential that delete [] be used, not delete . If delete is used erroneously, at best only the first element of the array will be deleted. You know that when an array of objects is created, the default constructor is used. This means that you will want to ensure that you have defined the default constructor to suit your needs. Remember that a compiler-generated default constructor does not initialize the classes's data members . Also, you will probably want to overload the assignment operator ( = ) so that you can safely assign object values to the array objects. A two-dimensional array can be created using code such as the following:

TBook** ShelvesOfBooks = new TBook*[NumberOfShelves]; for(int i=0; i<NumberOfShelves; ++i) { ShelvesOfBooks[i] = new TBook[NumberOfBooks]; }

To delete such an array use the following:

for(int i=0; i<NumberofShelves; ++i) { delete [] ShelvesOfBooks[i]; } delete [] ShelvesOfBooks;

One thing remains unsaid: If you want to have an array of objects, a better approach is to create a vector of objects using the vector template from the STL. It enables any constructor to be used and also handles memory allocation and deallocation automatically. It will also reallocate memory if there is a memory shortage. This means that the use of the C library function realloc() is no longer required.

Placement new (allocation at a predetermined memory location) and nothrow new (does not throw an exception on failure, it returns NULL instead) have not been discussed because they are beyond the scope of this section.

Understand and Use C++-Style Casts

The four C++ casts are outlined in Table 3.1.

Table 3.1. C++-Style Casts

Cast

General Purpose

static_cast<T>( exp )

Used to perform casts such as an int to a double . T and exp can be pointers, references, arithmetic types (such as int ), or enum types. You cannot cast from one type to another; for example, from a pointer to an arithmetic.

dynamic_cast<T>( exp )

Used to perform casting down or across an inheritance hierarchy. For example, if class X inherits from class O, a pointer to class O can be cast to a pointer to class X, provided the conversion is valid. T can be a pointer or a reference to a defined class type or void* . exp can be a pointer or a reference. For a conversion from a base class to a derived class to be possible, the base class must contain at least one virtual function; in other words, it must be polymorphic.

One important feature of dynamic_cast is that if a conversion between pointers is not possible, a NULL pointer is returned; if a conversion between references is not possible, a std::bad_cast exception is thrown (include the header file <typeinfo> ). As a result, the conversion can be checked for success.

const_cast<T>( exp )

This is the only cast that can affect the const or volatile nature of an expression. It can be either cast off or cast on. This is the only thing const_cast is used for.

For example, if you want to pass a pointer to const data to a function that only takes a pointer to non- const data, and you know the data will not be modified, you could pass the pointer by const_cast ing it.

T and exp must be of the same type except for their const or volatile factors.

reinterpret_cast<T>( exp )

Used to perform unsafe or implementation-dependent casts. This cast should be used only when nothing else will do. This is because it enables you to reinterpret the expression as a completely different type, such as to cast a float* to an int* . It is commonly used to cast between function pointers. If you find yourself needing to use reinterpret_cast , decide carefully if the approach you are taking is the right one, and remember to document clearly your intention (and possibly your reasons for this approach).

T must be a pointer, a reference, an arithmetic type, a pointer to a function, or a pointer to a member function. A pointer can be cast to an integral type and vice versa.

The casts most likely to be of use are static_cast (for trivial type conversions such as int to double ) and dynamic_cast .

An example of using static_cast can be found in the last line of the following code:

int Sum = 0; int* Numbers = new int[20]; for(int i=0; i<20; ++i) { Numbers[i] = i*i; Sum += Numbers[i]; } double Average = static_cast<double>(Sum)/20;

One of the times when dynamic_cast is commonly used in C++Builder is to dynamic_cast TObject* Sender or TComponent* Owner , to ensure that Sender or Owner is of a desired class, such as TForm . For example, if a component is placed on a form, it can be necessary to distinguish if it was placed directly or was perhaps placed on a Panel component. To carry out such a test, the following code is required:

TForm* OwnerForm = dynamic_cast<TForm*>(Owner); if(OwnerForm) { //Perform processing since OwnerForm != NULL, i.e. 0 }

First a pointer of the required type is declared, and then it is set equal to the result of the dynamic_cast . If the cast is unsuccessful, the pointer will point to the required type and can be used for accessing that type. If it fails, it will point to NULL and, hence, can be used to evaluate a Boolean expression. Sender can be similarly used. The situations that require such casting are many and varied. What is important is to understand what it is that you want to achieve and make your intention and reasoning clear.

Each of the C++ casts performs a specific task and should be restricted for use only where appropriate. The C++ casts are also easily seen in code, making it more readable.

Know When to Use the Preprocessor

It is not appropriate to use the preprocessor for defining constants or for creating function macros. Instead, you should use const variables or enum types for constants and use an inline function (or inline template function) to replace a function macro. Consider also that a function macro might not be appropriate anyway (in which case the inline equivalent would not be required).

For example, the constant p can be defined as

const double PI = 3.141592654;

If you wanted to place this inside a class definition, you would write

class Circle { public: static const double PI; // This is only a declaration };

In the implementation ( *.cpp ) file, you would define and initialize the constant by writing

const double Circle::PI = 3.141592654; // This is the constant definition // and initialization

Note that the class constant is made static so that only one copy of the constant exists for the class. Also notice that the constant is initialized in the implementation file (typically after the include directive for the header file that contains the class definition). The exception to this is the initialization of integral types, char , short , long , unsigned , and int . These can be initialized directly in the class definition. When a group of related constants is required, an enum is a sensible choice:

enum LanguagesSupported { English, Chinese, Japanese, French };

Sometimes an enum is used to declare an integer constant on its own:

enum { LENGTH = 255 };

Such declarations are sometimes seen inside class definitions. A static const variable declaration (like that for PI ) is a more correct approach.

Replacing a function macro is also easily achieved. Given the macro

#define cubeX(x) ( (x)*(x)*(x) )

the following inline function equivalent can be written:

inline double cubeX(double x) { return x*x*x; }

Notice that this function takes a double as an argument. If an int were passed as a parameter, it would have to be cast to a double . Because you want the behavior of the function to be similar to that of the macro, you should avoid this necessity. This can be achieved in one of two ways: Either overload the function or make it a function template. In this case, overloading the function is the better of the two choices because a function template would imply that the function could be used for classes as well, which would most likely be inappropriate. Therefore, an int version of the inline function could be written as

inline int cubeX(int x) { return x*x*x; }

Generally, you want to avoid using #define for constants and function macros. #define should be used when writing include guards. Remember that include guards are written in the header file to ensure that a header already included is not included again. For example, a typical header file in C++Builder will look like this:

#ifndef Unit1H // Is Unit1H not already defined? #define Unit1H // If not then you reach this line and define it // Header file code placed here... #endif // End of if Unit1H not defined

This code ensures that the code between #ifndef and #endif will be included only once. It is a good idea to follow some convention when choosing suitable defines for header files. C++Builder uses an uppercase H after the header filename. If you write your own translation units, you should follow this convention. Of course, you can use a different naming convention, such as prepending INCLUDED_ to the header filename, but you should be consistent throughout a project. Using include guards prevents a header file from being included more than once, but it must still be processed to see if it is to be included.

TIP

When you follow the IDE naming convention for include guards (appending an ' H' to the end of the header filename), the IDE treats the translation unit as a set, and it will appear as such in the Project Manager. If you do not want your .cpp and .h files to be treated in this way, do not use IDE-style include guards.

It has been shown that for very large projects (or more generally, projects with large, dense include graphs), this can have a significant effect on compile times. (For more information, see Large-Scale C++ Software Design by Lakos, 1996, p. 82.) Therefore, it is worth wrapping all include statements in an include guard to prevent the unnecessary inclusion of a file that has been defined already. For example, if Unit1 from the previous code snippet also included ModalUnit1 , ModalUnit2 , and ModalUnit3 , which are dialog forms used by other parts of the program, their include statements could be wrapped inside an include guard as follows:

#ifndef Unit1H // Is Unit1H not already defined? #define Unit1H // If not then you reach this line and define it #ifndef ModalUnit1H // Is ModalUnit1H not already defined? #include "ModalUnit1.h" // No then include it #endif // End of if Unit1H not defined #ifndef ModalUnit2H #include "ModalUnit2.h" #endif #ifndef ModalUnit3H #include "ModalUnit3.h" #endif // Header file code placed here... #endif // End of if Unit1H not defined

This is not pretty, but it is effective. Remember that you must ensure that the names you define for include guards must not match any name that appears elsewhere in your program. The define statement will ensure that it is replaced with nothing, which could cause havoc. That is why a naming convention must be agreed on and adhered to.

TIP

Note that the Project Manager in C++Builder 5 was improved to include an expandable list of header file dependencies for each source file included in a project. Simply click the node beside the source filename to either expand or collapse the list. Note that the header file dependency lists are based on the source file's .obj file, hence the file must be compiled at least once to use this feature. Also note that the list could be out of date if changes are made without recompilation.

In C++Builder 6, the relationship between .h and .cpp is extended to automatically loading both into the Source Code Editor and providing tabs at the bottom of the editor window, so you can easily switch from header to implementation and back.

Know when using the preprocessor will benefit the program, and when it won't. Use it carefully and only when necessary.

Learn About and Use the C++ Standard Library

The C++ Standard Library, including the Standard Template Library (STL), is a constituent part of ANSI/ISO C++, just as the definition for bool is. You can save a lot of unnecessary coding by learning to use its features in your programs. The Standard Library has an advantage over homegrown code in that it has been thoroughly tested and is fast, and it is the standard, so portability is a big bonus. Standard Library features are summarized in the following list:

Nearly everything in the Standard Library is a template, and most of the library consists of the STL, so it is very flexible. For example, the vector template class can be used to store any kind of data of the same type. As a result, it is a direct replacement for arrays in C++ and should be used in preference to arrays whenever possible.

In C++Builder 6, Borland introduced the STLPort open source C++ Standard Library, which should compile and operate in the same fashion as the old Rogue Wave implementation. STLPort will run both under Windows and Linux, so it is compatible with CLX programs.


   
Top

Категории