Home

C++Builder 5 Developer's Guide



'; figDoc.write(zhtm); figDoc.close(); } // modified 3.1.99 RWE v4.1 -->
 

 Programming in C++Builder

  • Coding Style to Improve Readability

  • Better Programming Practices in C++Builder

  • Further Reading

This chapter introduces important concepts concerned with how code is written in C++Builder.

The first section concentrates on how the readability of code may be improved. Optimizing the readability of code is very important. More readable code is easier to understand and maintain, and this can have a big effect on the cost of a software project. It is more than just making code look pretty on a page; it is about augmenting the code's description such that its purpose, intent, and behavior are adequately reflected.

Adopting a coding style and applying it consistently is probably the easiest way to achieve more readable code. However, some styles offer greater improvement than others, and this section looks at a variety of approaches to several considerations in an effort to give the reader some insight into which styles they should adopt. More importantly, it offers suggestions as to why certain styles are possibly more effective than others as well as warnings on the application of certain styles. Some techniques in moderation are very effective but when overused will decrease readability. Situations such as this are highlighted. The fundamental elements of coding style are examined in turn, and guidelines are presented on how each element can affect readability, along with suggestions on how each element can be used to best effect. Where appropriate, effective use of the IDE is also covered.

The second section covers a variety of topics concerning the writing of C++ code in C++Builder applications. A wide range of topics is covered, from the use of new and delete to the use of const in programs. The main purpose of the section is to highlight areas of coding that are often misunderstood or misused. Often, certain concepts are not fully appreciated, particularly by programmers whose background is not in C++. However, the main target audience is those whose background is in C++. They will find explanations of specific issues concerning the use of C++ in C++Builder and should therefore benefit from the topics covered. Some topics begin with more straightforward material before moving the discussion to advanced concepts. Also, many of the topics address issues that arise as a result of the Object Pascal heritage of the VCL (Visual Component Library).

Finally, the last section, "Further Reading," lists the references that appear throughout the chapter, along with a brief description of the reference itself. You are encouraged to seek out these references; reading them should prove very beneficial.

Coding Style to Improve Readability

This section looks at some of the issues in improving the readability of code and offers possible methods of achieving this. Regardless of which styles and methods are chosen, the one thing that should be remembered is that you must be consistent. Consistency is very important (note that code in this section is written in different styles to illustrate points in the text).

The Use of Short and Simple Code

It may be obvious, but always try to keep any given block of code short and simple. This achieves two things.

First, it means that your code is ultimately the culmination of many smaller pieces of code, each of which serves a specific and understandable role. This means that at different parts of the code the complexity of the code is governed only by the level of abstraction, not by the amount of code present. Consider the two functions in Listing 3.1.

Listing 3.1 Code Complexity and Level of Abstraction

#include <vector>

double GetMaximumValue(const std::vector<double>& Vector) 
            throw(std::out_of_range)
{
  double Maximum = Vector.at(0);
  
  for(int i=0; i<Vector.size(); ++i)
  {
   if(Vector[i] > Maximum)
   {
     Maximum = Vector[i];
   }
  }

  return Maximum;
}

void NormalizeVector(std::vector<double>& Vector)
{
  if(!Vector.empty())
  {
   double Maximum = GetMaximumValue(Vector);
 
   for(int i=0; i<Vector.size(); ++i)
   {
     Vector[i] -= Maximum;
   }
  }  

}

Both of the above functions are similar in terms of the complexity of the code that they contain, but the second function, NormalizeVector, performs in total a more complex task, because the level of abstraction is higher.


Tip - Use the pre-increment operator instead of the post-increment operator—for example, in Listing 3.1, ++i is used in preference to i++. Using either of the increment operators will increment i, but the post-increment version will return the old value of i. Since this is not required, the extra processing time is wasted. Typically, the post-increment operator is implemented in terms of the pre-increment operator. The pre-increment operator is therefore faster than the post-increment operator.

For built-in types such as int, there is little difference in speed, but for user-defined types this may not be the case. Regardless, if the post-increment operator is repeatedly used unnecessarily, there will be a cost in terms of performance.


Second, it means that when you are reading code you are never too far away from local variable declarations and function parameters (which contain type information). To this end, if you find yourself writing large pieces of code that perform several tasks, consider how the code could be separated into smaller conceptual blocks and rewritten accordingly.

The Use of Code Layout

The easiest way to improve the layout of your code is to make sure that braces ({}) are placed on their own line and that matching pairs line up. Code inside braces should then be indented by a predetermined amount that is used consistently throughout the program. Typically, two to four spaces are used as a suitable indent, though some use more and some use only one. (Beware of too many spaces; this can actually degrade readability.)


Tip - Make sure the Use Tab Character setting on the General tab in Tools, Editor Options is unchecked. This ensures that spaces are inserted for tabs. In general, avoid using Tab; use spaces instead.


Why is this so helpful? First, it allows fast visual scanning of the code. The code can be broken quickly into its constituent functional blocks, and each block's nature can be seen from the line starting the block. Second, the scope of the code is very clear. It is obvious which variables are in or out of scope at any given time, and this can be helpful in tracking down problem code. This is an important feature of this approach. Remember that the scope of any given block includes the block header statement (such as a for statement or a function header), and the functional part of the block is the code contained by the braces. Aligning the braces with the block header statement and indenting the functional code show this logical relationship explicitly.

The reason this layout style is so effective is that it is easier to match a pair of braces than it is to match an end brace with a keyword (or other permutations), due to the lack of symbolic similarity. It is also easy to maintain, because each opening brace must have a corresponding ending brace in the same column.


Tip - The IDE allows you to indent and unindent selected blocks of code using, respectively, CTRL+SHIFT+I and CTRL+SHIFT+U. The number of spaces that the editor indents or unindents the selected code is determined by the value of the Block indent setting on the General tab in Tools, Editor Options. Setting this value to 1 (one) offers the greatest flexibility.


Indenting both the braces and the code contained in them together is not as effective, because the braces are hard to pick out in the code. For single blocks of code this is not such a problem, but for nested blocks it can become confusing. That said, some still prefer this approach. For an alternative discussion of this, please refer to A Practical Handbook of Software Construction by McConnell, 1993. Be aware that most of the discussion presented in the book refers to languages that use keywords to show control block structures, such as begin and end, and the considerations involved are therefore somewhat different, though the distinction is not considered by the text. Tread cautiously.

When code is laid out as previously described—braces on their own line, matching pairs of braces lined up, and code within the braces indented—it can be read easily. Loop constructs can be marked as blocks, and nested constructs become very clear. No room is left for ambiguity. One instance in which this can be particularly useful is in the use of nested if-else statements. The following code is unclear:

#include <ostream>

// We have : int A <= int B <= int C

if(A + B > C)
 if((A==B) || (B==C))
 if((A==B) && (B==C)) std::cout << "This is an EQUILATERAL triangle";
else
if( (A*A + B*B) == C*C )
   std::cout << "This is a RIGHT-ANGLED ISOCELES triangle";
 else std::cout << "This is an ISOCELES triangle";
else
  if( (A*A + B*B) == C*C )std::cout << "This is a RIGHT-ANGLED triangle";
  else std::cout << "This is a TRIANGLE";
  else std::cout << "This is NOT triangle";

Its meaning is straightforward when written as follows:

#include <ostream>

// We have : int A <= int B <= int C

if(A + B > C)
{
  if((A==B) || (B==C))
  {
   if((A==B) && (B==C))
   {
     std::cout << "This is an EQUILATERAL triangle";
   }
   else if( (A*A + B*B) == C*C )
   {
     std::cout << "This is a RIGHT-ANGLED ISOCELES triangle";
   }
   else
   {
     std::cout << "This is an ISOCELES triangle";
   }
  }
  else if( (A*A + B*B) == C*C )
  {
   std::cout << "This is a RIGHT-ANGLED ISOCELES triangle";
  }
  else std::cout << "This is a TRIANGLE";
}
else std::cout << "This is NOT triangle";

Always consider if there is a better, clearer way to write code, especially if you find yourself writing large nested if and if-else statements. If you are writing a large if-else block, consider replacing it with a switch statement. This may not always be possible. In the case of large nested if statements, try restructuring the code into consecutive if-else statements.

In a similar vein, try to keep lines of code short. This makes it easier to read in the editor window and also makes it easier to print out.


Tip - Often when code is printed, some code lines are too long for the page onto which they are printed. When this occurs, one of two things happens: The lines are wrapped to the start of the next line or the lines are simply truncated. Both are unsatisfactory and degrade the readability of the code.

The best way to prevent this is to ensure that excessively long lines of code either are avoided or that they are carefully broken to multiple lines. When you are writing code in the Code Editor, the right margin can be used as a guide to the width of the printable page that you use. Change the right margin setting on the Display tab in the Tools, Editor Options menu so that the value represents the absolute printable right margin. In other words, set it so that characters that appear after this margin either are not printed or are wrapped to the next line (if Wrap Lines is checked in the File, Print menu). This will depend on the page size used and the value of the left margin setting (also in the File, Print menu).

For A4 paper and a left margin setting of 0, the right margin should be set to 94. This means that code lines that extend past this line will not be printed as they appear on the screen. To ensure that the right margin is visible in the Code Editor, make sure that the Visible Right Margin setting on the Display tab in the Tools, Editor Options menu is also checked.


Common reasons why code lines can become excessively long include heavily nested loops or selection constructs, use of switch statements with complex code inside, trying to write for and if statements on the same line, long function parameter lists, long Boolean expressions inside if statements, and string concatenation using the + operator.

For heavily nested loops or selection constructs, decrease your indent size if it is large or redesign the code so that some of the work is carried out by functions.

switch statements can be written differently. For example

switch(Key)
{
 case 'a' : // a very long line of code that disappears ...
       break;
 case 'b' : // another very long piece of code ...
       break;
 default : // Value not required – default processing
       break;
}

This can be rewritten as

switch(Key)
{
 case 'a' 

 : // a very long line of code that can now all be seen
  break;
 
 case 'b' 

 : // another very long piece of code that can also be seen
  break;

 default

 : // Value not required – default processing
  break;
}

With for and if statements all on one line, place the code executable part of the statement on a separate line. For example

for(int i=0; i<10; ++i) // long code that disappears ...

This can be replaced by the following:

for(int i=0; i<10; ++i)
{
  // long piece of code that no longer disappears 
}

The if statement can be similarly written. For example

if( Key == 'a' || Key == 'A' ) // long code that disappears ...

This can be replaced with

if( Key == 'a' || Key == 'A' )
{
 // long piece of code that no longer disappears 
}

In fact, it is better practice to write such one-line statements in this way, because it allows the debugger to trace into the line of code that is to be executed.

Long Boolean expressions inside if statements should be written on several lines, such as in the following:

if(Key == VK_UP || Key == VK_DOWN || Key == VK_LEFT...
{
 // Code here
}

This is better written as follows:

if(Key == VK_UP 
  || Key == VK_DOWN 
  || Key == VK_LEFT
  || Key == VK_RIGHT
  || Key == VK_HOME 
  || Key == VK_END)
{
 // Code here
}

This code raises an important point concerning the placement of the || or similar operator. The reason for placing it on the left side of each line is that we read from left to right. Placing it on the right makes the reading both slower and less natural, and it distorts the emphasis of the expression. Placing the operator on the right side of each line in the expression to show that there is more after the line is unnecessary, because people tend to read code by scanning blocks, not by reading individual lines as a computer does.

Long function parameter lists can be dealt with similarly, by placing each parameter on a new line. For example

void DrawBoxWithMessage(const AnsiString &Message, int...

This can be rewritten as shown in Listing 3.2.

Listing 3.2 Writing Long Function Parameter Lists

void DrawBoxWithMessage(const AnsiString &Message, 
            int Top,
            int Left,
            int Height,

            int Width);

It is important to place the comma at the end of each line. Placing the comma at the start of each line makes the code more difficult to read, because you would not expect to encounter a comma in this position in normal written text. The comma is for the compiler to separate the parameters and has no other meaning; its use should not unduly confuse someone reading the program. Note that the positioning of the comma is in contrast to the previous discussion of operator placement.

The same approach can be taken with long string concatenations:

AnsiString FilePath = "";
AnsiString FileName = "TestFile";

FilePath = "C:\\RootDirectory" + "\\" + "Branch\\Leaf" + ...

This can be rewritten as shown in Listing 3.3.

Listing 3.3 Writing Long String Concatenations

AnsiString FilePath = "";
AnsiString FileName = "TestFile";

FilePath = "C:\\RootDirectory" 
      + "\\" 
      + "Branch\\Leaf" 
      + FileName

      + ".txt";

The code in Listing 3.2 and Listing 3.3 is very clear, but it may not be very easy to maintain due to the amount of indentation required. For those who find adding lots of spaces difficult (some people really do!), an alternative approach is to use the standard indent for each of the following lines in such an expression. For example, if you are using a three-space indent, then the code from Listing 3.2 would become

void DrawBoxWithMessage(const AnsiString &Message, 
  int Top,
  int Left,
  int Height,
  int Width);

This degrades readability but results in code that is easier to maintain, a rather dubious trade-off, but sometimes a reasonable one.


Tip - You can save time writing code by using the code templates. These are accessed from the editor by pressing Ctrl+J and then using the up and down arrow keys or the mouse to select a template. However, remember that in order to maintain consistency with your own coding style and to get the best use of code templates, you should customize the templates that C++Builder provides. To edit code templates, go to Tools, Editor Options and select the Code Insight tab. Note that the | character is used to indicate where the cursor will be placed initially when the template is pasted into the editor. Code templates can also be edited manually in the $(BCB)\Bin\Bcb.dci file (where $(BCB) is the C++Builder 5 installation directory).


The Use of Sensible Naming

One of the best ways to improve the readability of code is to use sensible naming for variables, types, and functions. Type names include those used for naming actual types; for example int is the type name for an integer, TFont is the type name for the VCL's font class. Variable names are the names of variables that are declared to be of a specific type, for example, in the code

int NumberOfPages;

NumberOfPages is the name of a variable of type int.

Function names are given to functions to describe what they do. We'll consider variable names first, though most of what is said about variable names applies equally to type names and function names.

Choosing Variable Names to Indicate Purpose

Generally, you want to choose a name that reflects a variable's nature and purpose. If possible, it should also suggest what type the variable is. For example, String EmployeeName; is better than String S;.

When the variable pops up later in code, you will have no idea what S is for. For example, is it the number of pages in a book or a string representing a person's name? You also won't know what it is—for example, an int, a double, a String? EmployeeName is obvious: It is a variable that holds the name of an employee, and it is most likely a string.

Using descriptive names such as this is easy to do and makes everyone's life much easier when the code is read at a later date. Every time a new variable is declared, ask the question, "What is the variable's purpose?" Summarize the answer to the question and you have a sensible variable name.

When naming a variable, a word or short phrase is often used. Using a capital letter to start each word in the variable name is a popular method of making the name clear and easy to read. Others prefer all lowercase with underscores to separate words and, still others, a mixture of both. To illustrate, the previous declaration could be written as any of the following:

String EmployeeName;
String employee_name;
String Employee_Name;
String employeeName;

A disadvantage of using underscores is that variable names can quickly become very long. These are okay technically, but they start to make code look cluttered and also increase the symbolic appearance of variable names. Some suggest that all variable names should start with a lowercase letter, such as the employeeName example. This is often done to separate variable names from type names, which would typically start with a capital letter. Others start a variable name with a lowercase letter only when it is a temporary variable, such as a temp variable in a swap function. A compromise should be found so that a variable name is meaningful but also concise. That said, the meaningfulness of a variable name is most important.

Modifying Variable Names to Indicate Type

In general, knowing the purpose of a variable is often more important to the understanding of a piece of code than knowing what type the variable is. However, there are times when you may want to remind yourself of the variable type because special rules might apply. A simple example to illustrate this is given in Listing 3.4.

Listing 3.4 Illustrating the Need to Be Aware of a Variable's Type

int Sum = 0;
int* Numbers = new int[20];

for(int i=0; i<20; ++i)
{
  Numbers[i] = i*i;
  Sum += Numbers[i];
}


double Average = Sum/20; // Sum is an int, needs cast as a double

The answer to the code in Listing 3.4 should be 123.5, but the value in Average will be 123. This is because Sum is an int, and when you divide by 20 (treated as an int), you get an int result that is assigned to Average.


Tip - Try to declare variables just before they are used; this helps ensure that variables are created only if they are actually needed. If code contains conditional statements (or throws an exception), it may be that some of the variables declared are not used. It is sensible not to incur the cost of creating and destroying such variables unless they are used. Be wary, though, of placing declarations inside loops, unless that is what is intended. It is also a good idea to initialize variables when they are declared. On a similar note, never mix pointer and non-pointer declarations on the same line. Doing so is confusing at best.

It is preferable to have variables declared as

Type VariableName;.

Hence, a pointer to an int should be written as

int* pointerToInt;

But if we write

int* pointerToInt, isThisAPointerToInt;

then PointerToInt is a pointer to an int and isThisAPointerToInt is not a pointer to an int, it is an int. Actually, this is what the declaration is saying:

int *pointerToInt, notPointerToInt;
This is still not clear, and the declaration is no longer written in the Type VariableName format. xThe solution is to write the declarations on separate lines. The ambiguity then disappears.x
int* pointerToInt = 0;
int notPointerToInt;

You should ensure that pointers are explicitly initialized either to NULL (0) or to some valid memory location. This prevents the accidental use of wild pointers (those that point to an undefined memory location).


To obtain the expected result of 123.5 for Average in Listing 3.4, the last line of the code snippet should be as follows:

double Average = static_cast<double>(Sum)/20; // Performs as expected

This results in Sum being cast as a double before being divided by 20 (treated as a double). Average is now assigned the double value 123.5.


Tip - Make sure you are familiar with the four types of C++ casts, and always use C++-style casting in preference to C-style casting. C++-style casts are more visible in code and give an indication of the nature of the cast taking place.


This example is somewhat trivial, and adding type information to the variable name would be a bit like using a sledgehammer to crack a nut. A more sensible solution is probably to declare Sum as a double.

Sometimes type information may be required. One method of adding type information to a variable name is to use a letter (or letters) as a symbol at the start or at the end of each variable name. For example, you might use b for bool, s for a string, and so on. A problem with this approach is that type information can be added unnecessarily to too many different types of variables. The emphasis is then invariably placed on the type information and not on the variable's purpose. Also, there are many more possible types for a variable than there are letters of the alphabet. As a result, such symbols can themselves become confusing and complex. An infamous example of such a convention is the Hungarian Notation commonly seen in Win32 API code.


Note - The Hungarian Notation has its roots in Win API programming and advocates adding symbols to all variables to indicate their type. Since there aren't enough letters for all types, oddities in the notation are common. For example, Boolean variables are prefixed with an f, strings with an sz, pointers with p, and so on. A complete list is not appropriate here. The notation is infamous because many feel it creates more problems than it solves. These arise mostly from inconsistencies (such as the variable wParam being a 32-bit unsigned integer, not a 16-bit unsigned integer as the prefix implies) and names that are difficult to read. A perusal of the Win32 API help files should reveal some examples of the notation. Heavily typed notations are dangerous because they place the emphasis of a variable name on the variable's type, which often does not tell you much about the variable.


When reading such code, it is not always easy for the reader to mentally strip away the type codes, and this can decrease the code's overall readability. If you want prefixed (or even appended) type symbols, a compromise is to restrict the use of added symbols to only a few specific types.

Modifying Variable Names to Indicate Characteristics or Restrictions

A variable's name can also be used to convey information regarding some characteristic that a variable may have or to point out some restriction that may be applicable. It is important to know when a variable is a pointer, because pointers can easily wreak havoc in a program. For example, if you have a pointer to an int and accidentally add 5 to it without dereferencing it first, you have a problem. It is good practice to use a pointer only when no other type can be used, such as a reference (which are implicitly dereferenced).

It is also sometimes important to know when a variable is static, when a variable is a class data member, or when a variable is a function parameter. Suitable symbols that could be prefixed would be p_ for a pointer, s_ for a static variable, d_ or m_ for a class data member, and a_ for a function parameter. (An a_ symbol is used to differentiate a function parameter from a pointer and should be read as "where the parameter a_x represents the argument passed to the function." The use of a_ then becomes reasonable.)

An underscore is often used to separate a prefix from the variable name proper. This makes it easier to strip away the prefix when reading the code. If a separating underscore is used with an information symbol, then it is possible to append the symbol to the end of the variable name. This has the advantage of allowing the variable name to be read more naturally. For example, when reading s_NumberOfObjects, you would probably say "this is a static variable that holds the number of objects," whereas reading NumberOfObjects_s, you might say "this variable holds the number of objects and is static." This places the emphasis on the purpose of the variable (to store the number of objects). Which you prefer is probably a matter of personal choice. A problem with all such symbols occurs when more than one is applicable; then the syntax is not so tidy. Solutions such as always prefixing p_ for pointers and appending the other symbols can solve most of these problems, but not all.

Another common situation that often receives special attention is the naming of variables whose values do not change, in other words constants. Such variables are often named using all capital letters.

When you declare variables of certain classes, it is often sensible to include the class name (without any prefixed letter, for example T or C, if it is present; this is explained shortly) as part of the variable name. This is often done by prefixing some additional information to the name, or it can be as crude as appending a number to the class name as the C++Builder IDE does, though generally a little more consideration should be applied. For example, consider the following variable declarations.

TComboBox* CountryComboBox;
TLabel* NameLabel;
String BookTitle;

In the case of TComboBox and TLabel, it is appropriate to include the class name as part of the variable name. However, in the case of BookTitle, it is fairly obvious that it is a string, and adding the word String to the variable name perhaps does more harm than good.

Of special note in C++Builder is the naming of private member variables, which have a corresponding __property declaration. By convention, such variables are prefixed with the letter F (for Field). You should follow this convention and avoid using a capital F prefix for other purposes. The purpose of a prefix in this case is to allow the property name to be used unchanged (it is not necessary to think of a similar-sounding variant name).

Choosing Type Names

As was mentioned earlier, the naming of types should be approached in a fashion similar to the naming of variables. However, some conventions need to be considered.

For classes, convention says that if the class derives from TObject, then the class name should be prefixed with the letter T. This lets the user of the class know that it is a VCL-style class, and it is consistent with the naming of other VCL classes. This has a beneficial side effect. Variables declared of the class can use the class name without the prefix, making it obvious what the variable is. Non-VCL (that is, normal C++) classes can also use a prefix, but it is wise perhaps to use a prefix other than T, such as C to indicate that the normal C++ object model applies and reinforce that the class does not descend from TObject. This distinction can be important: VCL-style classes must be created dynamically; non-VCL classes do not have this restriction.

The naming of other types, such as enums, Sets, structs, and unions, can be handled in a similar fashion. By convention, C++Builder prefixes a T to enumeration and Set names, though some may prefer not to follow this convention, which has no specific meaning. Avoid using an E as a prefix; C++Builder uses this for its exception classes.

Because enums and Sets are commonly used in C++Builder, it is worth mentioning some points specifically related to their naming. A Set is a template class declared in $(BCB)\Include\VCL\sysset.h as

template<class T, unsigned char minEl, unsigned char maxEl> 
 class RTL_DELPHIRETURN Set;

Ignoring RTL_DELPHIRETURN, which is present for VCL compatibility, we can see that a Set template takes three parameters: a type parameter and two range-bounding parameters. Hence, a Set could be declared as

Set<char, 'A', 'Z'> CapitalLetterMask;

If a Set is to be used more than once, a typedef is normally used to simplify its representation, as seen here:

typedef Set<char, 'A', 'Z'> TCapitalLetterMask;
// Later in the code
TCapitalLetterMask CapitalLetterMask;

Sets are often used to implement masks (as in this case), hence the use of the word Mask in the names used in the previous code. An enumeration is typically used to implement the contents of a Set. For example, the following definitions can be found in $(BCB)\Include\VCL\graphics.hpp:

enum TFontStyle { fsBold, fsItalic, fsUnderline, fsStrikeOut };

typedef Set<TFontStyle, fsBold, fsStrikeOut> TFontStyles;

Most enums used by the VCL are to facilitate the use of Sets. For convenience they are declared at file scope. Simply including the file allows easy access to the Set. This means that the potential for a name collision is high. To avoid such collisions, the "initials" of the enum name are prefixed to each of the values that the enum can take. This minimizes the chance of a name collision. This is good practice and should be used in your own code. For example, the initials of TFontStyle (excluding the T) are fs, which is prefixed to each of the enums. Another method that can be used to prevent name collisions is to place enums and typedefs inside the class definitions that use them. This means that such enums and typedefs, when called from outside the class, must be qualified by the class namespace. The same can be applied to const values.

Using a typedef in certain situations (such as this one) can improve readability. A typedef can also improve readability in the declaration of function pointers (and particularly __closures, i.e. events, discussed in Chapter 9, "Creating Custom Components"). Beyond situations such as these, typedefs should be used sparingly because you are actually hiding the type of the variable. Using typedefs too much will result in confusion.

Choosing Function Names

Function names should be precise and describe what the function does. If it is not possible to describe precisely what a function does, then perhaps the function itself should be changed. Different kinds of functions should be named in slightly different ways.

A function that does not return a value (a void function) or returns only function success information—in other words, those that return no data—should generally be named using an object verb name, such as CreateForm(), DisplayBitmap(), and OpenCommPort(). A member function of this type often does not require a qualifying object, because the object that calls the function generally can fulfill that role.

If a function does return a data value, then the function should be named to reflect the nature of the return value, such as GetAverage(), log10(), and ReadIntervalTimeOut().

Some prefer to make the first word in a function name lowercase. This is a matter of personal preference, but consistency should be maintained.

In general, function names can be longer than variable names, and if you need to write a long function name, you should not be overly concerned. However, take care to ensure that a long function name is not the product of a poorly designed function that tries to perform too many poorly related operations. In fact, you should endeavor to write functions that perform a single well-defined task, and the name of the function should reflect that. If you must write a function that does more than one thing, the name should make that obvious.

Adhering to Naming Conventions

How variables, types, and functions are named is an extensive topic, encompassing a myriad of concerns. Mentioning every convention that could be used is impossible. That being the case, you should endeavor to adhere to the following guidelines:

  • Name a variable or a function such that its purpose is obvious. If this means more typing, then so be it.

  • Name a type so that its intended use is obvious. Try not to use a name that is an obvious choice for a variable of the type. Prefixing a letter symbol can help.

  • Be consistent! With practice, even the most obscure naming system can be understood if it is consistently applied.

The Use of Code Constructs

One of the best ways to improve the readability of code is through the appropriate use of code constructs—using the right tool at the right time for the right job. Therefore it is important to understand when to use const (and when not to), when to use references instead of pointers, which loop statement is most appropriate, whether to use multiple if-else statements or a single switch statement, when to represent something with a class and when not to, when to throw an exception and when to catch it, how to write an exception specification, and so on. Most of these are design issues, but some are simple to add to code and can improve not only readability but also robustness. The use of const and references (along with other coding issues) is discussed in the next section. This is an area of programming in which you can continually improve.

The bottom line is this: If you know what you are doing when you write code, and you are doing it for the right reasons and in the right way, then your code will be easier to follow. This is because what the reader expects to happen will happen. If it doesn't, the reader will become confused by the code, which is to be avoided.

The Use of Comments

The main purpose of comments is to allow the annotation of the code to improve readability. Judicious use of comments does just that, but care needs to be taken not to make the code untidy by putting comments here and there without any real strategy.

Comments can be applied throughout implementation code to annotate specific areas where confusion might arise. If you do this, use only C++-style comments beginning with //. This prevents comments from being finished prematurely by the unexpected occurrence of a C-style comment end, */.

Using Comments to Document Code

How comments are added to implementation code is important. If it is done poorly, the effect can be to make code even more unclear. If a few guidelines are followed, comments can improve the readability of code.

It is important not to interrupt the layout of the code. Comments should be separated from the code, either with space or a differing style (such as italic) or by using a different color. A comment should be indented when code is indented. This is particularly important if the comments discuss the code's functionality. Comments written in this way can be scanned independently from the code, and the code can be scanned independently from the comments. Some programmers prefer not to separate a comment line from a code line, because they feel that use of color and style is sufficient to separate the comments from the code.


Tip - By modifying the settings on the Colors tab in the Tools, Editor Options menu, it is possible to remove or highlight comments as required. To hide a comment from view in the editor window, simply change the color of the comment so that it matches the editor window background color; in the Defaults setting this is white. To highlight a comment, try changing the background color to blue and setting the foreground color to white. You can experiment to see what you like most.


If a comment is specifically related to a single line of code and room permits, it is better to add the comment on the same line following the code. If several lines require such comments, an effort should be made to ensure that each comment starts at the same column in the editor. The advantage of such comments is that they do not interfere with the code's layout. For example, code could be commented as follows:

double GetMaximumValue(const std::vector<double>& Vector) 
       throw(std::out_of_range)
{
  // Initialize Maximum, if the vector is empty an 
  // std::out_of_range exception will be thrown

  double Maximum = Vector.at(0);   
  
  for(int i=0; i<Vector.size(); ++i)
  {
   if(Vector[i] > Maximum)  // If the ith element is greater 
   {             // than the current value in
     Maximum = Vector[i];  // Maximum then set Maximum equal 
   }             // to its value 
  }
  
  return Maximum;
}

The comments for the if statement are superfluous, but they have been added to illustrate how comments can be laid out. Comments can also be appended to the closing brace of loop and selection statements:

}//end if
}//end for
}// ! for
}//end for(int i=0; i<Vector.size(); ++i)

The last example might be too much sometimes, but it can be useful when writing code or when a loop or selection construct spans more than one page.

Comments are very useful when documenting code. They are particularly useful for summarizing important information about a function's operational characteristics. There are many ways to present such information, but the important thing is to use one method consistently. If there is something important to say about a function, the information should appear in the header file that declares it and in the implementation file that implements it. Whoever maintains the code needs as much information as the user of the code. You should always have a brief summary of a function's purpose above the function implementation and at the function's declaration. You should also list any requirements that must be met for the function to operate as expected (sometimes referred to as preconditions) and any promises that the function makes to the user (sometimes referred to as postconditions). Any condition that results in undefined behavior should be explicitly stated and included in any list of requirements. Information presented in this way can be thought of as a contract for the function—if you do this, then the function promises to do that.

Only enough information as is necessary needs to be documented. For short, simple functions, little needs to be written. Conversely, large complex functions may require more lengthy comments. If this is so, two considerations must be kept in mind. Users of a function don't need to know of implementation issues internal to the function or how a function affects any private or protected data (assuming the function is a class member function). Such information should not appear in a header file. Also remember that comments are most useful when they are close to where they are directed in the implementation. It may be that some of the description is too detailed and some of the information given would be better placed elsewhere. An example of commenting a function in the implementation file would be as follows:

//-----------------------------------------------------------//
//
// PURPOSE : Returns the maximum value present in a vector 
//      of doubles
//
// REQUIRES : The vector passed is not empty
//
// PROMISE : The function will return the value of the 
//      largest element
//
//-----------------------------------------------------------//
  
double GetMaximumValue(const std::vector<double>& Vector) 
        throw(std::out_of_range)
{
  double Maximum = Vector.at(0); 
  
  for(int i=0; i<Vector.size(); ++i)
  {
   if(Vector[i] > Maximum) 
   { 
     Maximum = Vector[i];
   } 
  }
  
  return Maximum;
}

Similarly, the header file could look like this:

double GetMaximumValue(const std::vector<double>& Vector) 
         throw(std::out_of_range)
  // PURPOSE : Returns the maximum value in a vector of doubles
  // REQUIRES : The vector passed is not empty
  // PROMISE : Returns the value of the largest element

Remember that the header files in a program are often the only up-to-date documentation available for an interface, and as such they should be clear and accurately maintained (kept up-to-date). If this is not done, comments can quickly become useless. If code is changed, then always change any comments that relate to that code.

If you find yourself having to write extensive comments to explain a particularly tricky piece of code, it may be that the code itself should be changed.

Using Comments to Ignore Code

Another use of comments of particular note to C++Builder is the commenting out of the names of unused parameters in IDE-generated event handlers. It is common that some or all of the parameters are not used by the function. Commenting out the parameter names does not alter the function signature, but it does show explicitly which of the parameters are actually used. If parameters are written on the same line, then C-style comments must be used to achieve this. The parameter list can be rearranged to allow commenting with C++-style comments, but the result looks untidy. Therefore, caution must be exercised. The following code snippet of an event handler for a TButton MouseUp event is shown for illustration:

void __fastcall TMainForm::Button1MouseUp(TObject* /*Sender*/,
                     TMouseButton /*Button*/,
                     TShiftState /*Shift*/,
                     int X,
                     int Y)
{
  // Display the cursor position within Button1
  Label1->Caption.sprintf("%d,%d", X, Y);
}

Tip - The MouseUp event handler uses the sprintf() AnsiString member function to modify the Caption property of Label1. This works because the sprintf() member function returns the AnsiString (*this) by reference. In general, however, a property should be modified only by assigning a new value to it using the assignment operator.


It is possible to delete the unused parameter names, but this can make the function header confusing, especially if you decide to use one of the parameters at a later date. A solution is to comment out the parameter name and then replace the commented out comma or closing bracket before the comment, as follows:

void __fastcall TMainForm::Button1MouseUp(TObject* ,//Sender,
                     TMouseButton ,//Button,
                     TShiftState ,//Shift,
                     int X,
                     int Y)
{
  // Display the cursor position within Button1
  Label1->Caption.sprintf("%d,%d", X, Y);
}

This is effective and easily maintainable. If you want to use the parameter later, simply delete the // and the extra comma or extra closing bracket. An alternative is as follows:

void __fastcall TMainForm::Button1MouseUp(TObject* ,//Sender
                     TMouseButton ,//Button
                     TShiftState ,//Shift
                     int X,
                     int Y)
{
  // Display the cursor position within Button1
  Label1->Caption.sprintf("%d,%d", X, Y);
}

This differs only in the removal of the redundant commas from the end of each comment. You may find this is an improvement, because a comma (or bracket) at the end of a comment could be a distraction.

Using Comments to Improve Appearance

A final use of comments is to help improve the overall appearance of the code as it appears on the screen or when it is printed. This is done by placing boxes around headings, placing divider lines between functions, and so on. When using comments in this way, care should be taken not to obscure the code within a forest of * characters or other such symbols. You must also be consistent for this to be useful.

C++Builder automatically places a divider line between blocks of code that it generates. This helps improve the appearance of the code, because there is additional visual separation. It is good practice to do this with your own code.


Tip - C++Builder 5 lets you change the default divider line that it places between sections of code that it generates. You can do so by editing the following entry in the BCB.BCF file in the $(BCB)\BIN folder. If the entry does not exist, you will have to create it. As you can see, the format is similar to an *.ini file.

[Code Formatting]
Divider Line=//---- My Custom Divider Line ----//

The divider line should begin as a comment line, with //. A good method of adding the line you want is to first write it in the Code Editor, then cut and paste it to the BCB.BCF file. That way you know exactly what it will look like. This helps IDE-generated code look consistent with your own code. It is a good idea to add this divider line to your code templates.


A Final Note on Improving the Readability of Code

Ultimately, the best descriptor of the code's purpose and how it operates is the code itself. By improving the code's readability, you enhance this description.

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++. A list of suggested reading is given at the end of this chapter. 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 typedefed to String). You also could use both. You can access the C++ Standard Library's string class by including the statement

#include <string>

at the top of your code. If portability is a goal, this is the string class to use. Otherwise, use AnsiString, which has the advantage that it is the string representation used throughout the VCL. This allows your code to work seamlessly with the VCL. You should endeavor to become familiar with the methods that AnsiString offers. Because strings are required so often, this will pay itself back in terms of improved use and efficiency.

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 would require 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.


Caution - 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 the 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 also discussed.

A reference always refers to only one object, its referent, and it cannot be re-bound to refer to a different object ("object" in this context includes all types). A reference must be initialized on creation, and 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 thing. We 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 we change the value of Y or X, we 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 ints:

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, structs, 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, then by non-const reference or pointer. For example:

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 better than:

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

The first function is better for two reasons. First, the AnsiString parameter is passed by reference. This means that when the function is called, the AnsiString used as the calling argument is used, because only a reference is used by the function. 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);

However, the first is safer and faster. Note that the calling code need not be directly affected.

Functions can also return references, which has the side effect of the function becoming an lvalue (a value that can appear on the left side of an expression) for the referent. This also allows 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, such as 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 we 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.

What then of the pointers used in C++Builder's VCL? The reason behind the extensive use of pointers in the VCL is that the VCL is written in Object Pascal, which uses Object Pascal references. An Object Pascal 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 Object Pascal references. This is because an Object Pascal 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 Object Pascal 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 may 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, we improve our 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). 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-Orientated Software by Gamma et al., 1995). Patterns are a way of representing recurring problems and their solutions in object-based programs. For more on patterns, see Chapter 4, "Advanced Programming with C++Builder."

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, then 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, and Large-Scale C++ Software Design, Lakos, 1996), 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 re-think of the code would also be helpful. It may 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 allow the use of non-modal forms. Such 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 auto-create list, which adds the line

Application->CreateForm(__classid(TFormX), &FormX);

(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 in. 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 auto-create 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 Non-Modal form.

If the form is Modal, then 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 auto-created. Remove the Application->CreateForm entry from WinMain either by deleting it or by removing it from the auto-create 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();

Whichever technique you use, you are guaranteed that if the code terminates prematurely because an exception is thrown, FormX will be destructed automatically. 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, since 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.)

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 0, 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 decide only whether or not you want it auto-created. 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 may still be in use. For example, if the form is auto-created, write

FormX->Show();

Otherwise create and show it this way:

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

It is important not to create the form again after it has been auto-created, because this will overwrite the reference to the auto-created form. This means that the auto-created instance can no longer be accessed by the application and can result in the application crashing when an attempt to dereference the global pointer is made. It is advisable therefore to check the value of the pointer for equality with NULL (0) before creation:

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

This is possible because the form pointer is a global variable that is guaranteed to be initialized to zero. Using this technique ensures that the global pointer will always point to a valid location and will not be overwritten.

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.)

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 allows 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, though 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 chars, 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, then 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 consts.

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 or not 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 Books. 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

An exception is a mechanism for handling runtime errors in a program. There are several approaches that can be taken to handling runtime errors, such as returning error codes, setting global error flags, and exiting the program. In many circumstances, an exception is the only appropriate method that can be employed effectively, such as when an error occurs in a constructor. (For more information, see ANSI/ISO C++ Professional Programmer's Handbook: The Complete Language by Kalev, 1999.)

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, though 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 may 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 may 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 may 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 terminate() handler is

#include <stdexcept>

std::set_terminate(TerminateHandler);

When an exception is thrown that is not expected, then 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. Instead, check the "Further Reading" section at the end of this chapter for more information.

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, then 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 may 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. (See the section "The Use of Comments," earlier in this chapter, for a discussion of a function's promise. Also see C++ FAQs, Second Edition, Cline et al., 1999.)

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.) VCL exceptions cannot be caught by value. Also, it may 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, the form 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 Global Variables," earlier in this chapter.

If you are writing 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.)

Never throw an exception from a destructor, because the destructor may 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 are created dynamically in 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.) 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.


Caution - A Non-Plain Old Data (Non-POD) object is essentially any 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 allows 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.)


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.


Caution - 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 allows 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 may 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 may 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 (non-member or static member) called OutOfMemory to handle such occurrences, the necessary code would be

#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, then 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, we 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 from. 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, then 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 up. One approach is to pre-allocate a quantity of memory at the beginning of the program. If new fails to allocate enough memory, then this memory 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 pre-allocated 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. There are other approaches, and the reader is directed to the books in the "Further Reading" section for more information.

It is important to note that if you want to allocate raw memory only, then 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.) 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 each object is initialized by calling its default constructor. Deleting an array using delete performs the opposite task: It calls the destructor for each object and then deallocates the memory (using operator delete[ ]) for the array. So that 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, then at best only the first element of the array will be deleted. We 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' 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 allows 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 also no longer required. For more information on the vector template class, refer to the "Introduction to the Standard C++ Library and Templates" section in Chapter 4.

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

Understand and Use C++-Style Casts

There are four C++ casts. They 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 may be a pointer, a reference, an arithmetic type (such as int), or an enum type. You cannot cast from one type to another, such as 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, then a pointer to class O can be cast to a pointer to class X, provided the conversion is valid. T may be a pointer or a reference to a defined class type or void*. exp may 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 functionthat only takes a pointer to non-const data, and you know the data will not be modified, you could pass the pointer by const_casting 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 orimplmentation-dependent casts. This cast should be used only when nothing else will do. This is because it allows you to re-interpret 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;

The astute among you will recognize this as the code from Listing 3.4 earlier in this chapter.

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 may 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 may not be appropriate anyway (in which case the inline equivalent would not be required).

For example, the constant ž can be defined as

const double PI = 3.141592654;

If you wanted to place this inside a class definition, then 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 we want the behavior of the function to be similar to that of the macro, we 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, we 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 we 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 pre-pending 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.) Therefore, it is worth wrapping all include statements in an include guard to prevent the unnecessary inclusion of a file that has been already defined. 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 we 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 upon and adhered to.


Tip - Note that the Project Manager in C++Builder 5 has been improved to include an expandable list of header file dependencies for each source file included in a project. Simply click on 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.


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:

  • Exceptions, such as bad_alloc, bad_cast, bad_typeid, and bad_exception

  • Utilities, such as min(), max(), auto_ptr<T>, and numeric_limits<T>

  • Input and output streams, such as istream and ostream

  • Containers, such as vector<T>

  • Algorithms, such as sort()

  • Function objects (functors), such as equal_to<T>()

  • Iterators

  • Strings, such as string

  • Numerics, such as complex<T>

  • Special containers, such as queue<T> and stack<T>

  • Internationalization support

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. For more information about the STL, refer to the "Introduction to the Standard C++ Library and Templates" section in Chapter 4.

Further Reading

This section lists some texts that cover the material presented in this chapter much more thoroughly, and they should be a first source for more information about C++ programming. The books are listed in alphabetical order according to the author's name. A brief summary of the contents of each book is also given.

  • Cline, M., Lomow, G., and Girou, M. (1999). C++ FAQs Second Edition. Addison-Wesley Longman, Inc.

    This text is based on the online C++ FAQ at but it offers much more than the online FAQ. It covers topics from basic to very advanced and is well written, having evolved over several years. This book offers very good value and makes an excellent reference, a great choice for a second book on C++.

  • Gamma, E., Helm, R., Johnson, R., and Vlissides, J. (1995). Design Patterns: Elements of Reusable Object-Orientated Software. Addison-Wesley Longman, Inc.

    This is the pioneering text on design patterns. Examples are given in C++ (and Smalltalk), making it particularly useful. This book should help you to approach object-based programming in a different way.

  • Horton, I. (1998). Beginning C++: The Complete Language. Wrox Press.

    This text has complete coverage of ANSI/ISO C++. It is not compiler specific and is very up to date. It is precise and includes information not found in other books. It makes a very good first book on C++.

  • Kalev, D. (1999). ANSI/ISO C++ Professional Programmer's Handbook: The Complete Language. Que Corporation.

    This is a relatively recent text with many interesting insights into the C++ standard. It is a handy reference for many of the more advanced topics in C++. It focuses a lot on why certain features are present, offering an insight rarely found in many similar books.

  • Lakos, J. (1996). Large-Scale C++ Software Design. Addison-Wesley Longman, Inc.

    Despite the title, this text is essential reading for anyone involved in any but the most trivial C++ projects. Divided into three parts covering basics, physical design, and logical design, the book has many guidelines and principles scattered throughout (collated at the end for ease of reference) that make reading it instantly productive.

  • McConnell, S. C. (1993). Code Complete: A Practical Handbook of Software Construction. Microsoft Press.

    This text offers a very thorough treatment of how code is written. It is a definitive guide to this subject and should be read by anyone who really wants to examine and understand the merits of the different ways code is written. It does not present one particular method of writing code but rather comments on a variety of techniques.

  • Meyers, S. (1998). Effective C++ Second Edition: 50 Specific Ways to Improve Your Programs and Designs. Addison-Wesley Longman, Inc.

    Perhaps one of the most famous C++ books, it is written in an easy-to-read style, its size belies the great wealth of information it contains.

  • Meyers, S. (1996). More Effective C++: 35 New Ways to Improve Your Programs and Designs. Addison-Wesley Longman, Inc.

    The follow-up text to Effective C++ offers more pearls of wisdom to be digested by the avid programmer. Meyers' books are definitely a must read.

  • Sutter, H. (2000). Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions. Addison-Wesley Longman, Inc.

    As the title suggests, this is an advanced text based on the "C++ Guru of the Week" series, which can be found on the comp.lang.c++.moderated newsgroup. It covers many topics and is highly informative. Once several of the previous texts have been digested, thisbook's true value can be appreciated.

[back to top