Day 17 |
The compiler does not read your original source code file; it reads the output of the preprocessor and compiles that file. You've seen the effect of this already with the #include directive. This instructs the preprocessor to find the file whose name follows the #include directive, and to write it into the intermediate file at that location. It is as if you had typed that entire file right into your source code, and by the time the compiler sees the source code, the included file is there.
#define BIG 512you have instructed the precompiler to substitute the string 512 wherever it sees the string BIG. This is not a string in the C++ sense. The characters 512 are substituted in your source code wherever the token BIG is seen. A token is a string of characters that can be used wherever a string or constant or other set of letters might be used. Thus, if you write
#define BIG 512 int myArray[BIG];The intermediate file produced by the precompiler will look like this:
int myArray[512];Note that the #define statement is gone. Precompiler statements are all removed from the intermediate file; they do not appear in the final source code at all.
#define BIGLater, you can test whether BIG has been defined and take action accordingly. The precompiler commands to test whether a string has been defined are #ifdef and #ifndef. Both of these must be followed by the command #endif before the block ends (before the next closing brace).
#ifdef evaluates to TRUE if the string it tests has been defined already. So, you can write
#ifdef DEBUG cout << "Debug defined"; #endifWhen the precompiler reads the #ifdef, it checks a table it has built to see if you've defined DEBUG. If you have, the #ifdef evaluates to TRUE, and everything to the next #else or #endif is written into the intermediate file for compiling. If it evaluates to FALSE, nothing between #ifdef DEBUG and #endif will be written into the intermediate file; it will be as if it were never in the source code in the first place.
Note that #ifndef is the logical reverse of #ifdef. #ifndef evaluates to TRUE if the string has not been defined up to that point in the file.
1: #define DemoVersion 2: #define DOS_VERSION 5 3: #include <iostream.h> 4: 5: 6: int main() 7: { 8: 9: cout << "Checking on the definitions of DemoVersion, DOS_VERSION Â _and WINDOWS_VERSION...\n"; 10: 11: #ifdef DemoVersion 12: cout << "DemoVersion defined.\n"; 13: #else 14: cout << "DemoVersion not defined.\n"; 15: #endif 16: 17: #ifndef DOS_VERSION 18: cout << "DOS_VERSION not defined!\n"; 19: #else 20: cout << "DOS_VERSION defined as: " << DOS_VERSION << endl; 21: #endif 22: 23: #ifdef WINDOWS_VERSION 24: cout << "WINDOWS_VERSION defined!\n"; 25: #else 26: cout << "WINDOWS_VERSION was not defined.\n"; 27: #endif 28: 29: cout << "Done.\n"; 30: return 0; 31: } Output: Checking on the definitions of DemoVersion, DOS_VERSION Â _and WINDOWS_VERSION...\n"; DemoVersion defined. DOS_VERSION defined as: 5 WINDOWS_VERSION was not defined. Done.Analysis: On lines 1 and 2, DemoVersion and DOS_VERSION are defined, with DOS_VERSION defined with the string 5. On line 11, the definition of DemoVersion is tested, and because DemoVersion is defined (albeit with no value), the test is true and the string on line 12 is printed.
cout << "DOS_VERSION defined as: " << 5 << endl;Note that the first word DOS_VERSION is not substituted because it is in a quoted string. The second DOS_VERSION is substituted, however, and thus the compiler sees 5 as if you had typed 5 there.
Finally, on line 23, the program tests for WINDOWS_VERSION. Because you did not define WINDOWS_VERSION, the test fails and the message on line 24 is printed.
Your main() function will be in its own CPP file, and all the CPP files will be compiled into OBJ files, which will then be linked together into a single program by the linker.
Because your programs will use methods from many classes, many header files will be included in each file. Also, header files often need to include one another. For example, the header file for a derived class's declaration must include the header file for its base class.
Imagine that the Animal class is declared in the file ANIMAL.HPP. The Dog class (which derives from Animal) must include the file ANIMAL.HPP in DOG.HPP, or Dog will not be able to derive from Animal. The Cat header also includes ANIMAL.HPP for the same reason.
If you create a method that uses both a Cat and a Dog, you will be in danger of including ANIMAL.HPP twice. This will generate a compile-time error, because it is not legal to declare a class (Animal) twice, even though the declarations are identical. You can solve this problem with inclusion guards. At the top of your ANIMAL header file, you write these lines:
#ifndef ANIMAL_HPP #define ANIMAL_HPP ... // the whole file goes here #endifThis says, if you haven't defined the term ANIMAL_HPP, go ahead and define it now. Between the #define statement and the closing #endif are the entire contents of the file.
The first time your program includes this file, it reads the first line and the test evaluates to TRUE; that is, you have not yet defined ANIMAL_HPP. So, it goes ahead and defines it and then includes the entire file.
The second time your program includes the ANIMAL.HPP file, it reads the first line and the test evaluates to FALSE; ANIMAL.HPP has been defined. It therefore skips to the next #else (there isn't one) or the next #endif (at the end of the file). Thus, it skips the entire contents of the file, and the class is not declared twice.
The actual name of the defined symbol (ANIMAL_HPP) is not important, although it is customary to use the filename in all uppercase with the dot (.) changed to an underscore. This is purely convention, however.
NOTE: It never hurts to use inclusion guards. Often they will save you hours of debugging time.
It is common to put in special debugging code surrounded by #ifdef DEBUG and #endif. This allows all the debugging code to be easily removed from the source code when you compile the final version; just don't define the term DEBUG.
1: #define DemoVersion 2: #define DOS_VERSION 5 3: #include <iostream.h> 4: 5: 6: int main() 7: { 8: 9: cout << "Checking on the definitions of DemoVersion, DOS_VERSION Â _and WINDOWS_VERSION...\n"; 10: 11: #ifdef DemoVersion 12: cout << "DemoVersion defined.\n"; 13: #else 14: cout << "DemoVersion not defined.\n"; 15: #endif 16: 17: #ifndef DOS_VERSION 18: cout << "DOS_VERSION not defined!\n"; 19: #else 20: cout << "DOS_VERSION defined as: " << DOS_VERSION << endl; 21: #endif 22: 23: #ifdef WINDOWS_VERSION 24: cout << "WINDOWS_VERSION defined!\n"; 25: #else 26: cout << "WINDOWS_VERSION was not defined.\n"; 27: #endif 28: 29: #undef DOS_VERSION 30: 31: #ifdef DemoVersion 32: cout << "DemoVersion defined.\n"; 33: #else 34: cout << "DemoVersion not defined.\n"; 35: #endif 36: 37: #ifndef DOS_VERSION 38: cout << "DOS_VERSION not defined!\n"; 39: #else 40: cout << "DOS_VERSION defined as: " << DOS_VERSION << endl; 41: #endif 42: 43: #if_Tz'WINDOWS_VERSION 44: cout << "WINDOWS_VERSION defined!\n"; 45: #else 46: cout << "WINDOWS_VERSION was not defined.\n"; 47: #endif 48: 49: cout << "Done.\n"; 50: return 0; 51: } Output: Checking on the definitions of DemoVersion, DOS_VERSION Â _and WINDOWS_VERSION...\n"; DemoVersion defined. DOS_VERSION defined as: 5 WINDOWS_VERSION was not defined. DemoVersion defined. DOS_VERSION not defined! WINDOWS_VERSION was not defined. Done.
Another common use of this technique is to conditionally compile in some code based on whether debug has been defined, as you'll see in a few moments.
DO use conditional compilation when you need to create more than one version of your code at the same time. DON'T let your conditions get too complex to manage. DO use #undef as often as possible to avoid leaving stray definitions in your code. DO use inclusion guards!
#define TWICE(x) ( (x) * 2 )and then in your code you write
TWICE(4)The entire string TWICE(4) will be removed, and the value 8 will be substituted! When the precompiler sees the 4, it will substitute ( (4) * 2 ), which will then evaluate to 4 * 2 or 8.
A macro can have more than one parameter, and each parameter can be used repeatedly in the replacement text. Two common macros are MAX and MIN:
#define MAX(x,y) ( (x) > (y) ? (x) : (y) ) #define MIN(x,y) ( (x) < (y) ? (x) : (y) )Note that in a macro function definition, the opening parenthesis for the parameter list must immediately follow the macro name, with no spaces. The preprocessor is not as forgiving of white space as is the compiler.
If you were to write
#define MAX (x,y) ( (x) > (y) ? (x) : (y) )and then tried to use MAX like this,
int x = 5, y = 7, z; z = MAX(x,y);the intermediate code would be
int x = 5, y = 7, z; z = (x,y) ( (x) > (y) ? (x) : (y) ) (x,y)A simple text substitution would be done, rather than invoking the macro function. Thus the token MAX would have substituted for it (x,y) ( (x) > (y) ? (x) : (y) ), and then that would be followed by the (x,y) which followed Max.
By removing the space between MAX and (x,y), however, the intermediate code becomes:
int x = 5, y = 7, z; z =7;
#define MAX(x,y) x > y ? x : yand pass in the values 5 and 7, the macro works as intended. But if you pass in a more complicated expression, you'll get unintended results, as shown in Listing 17.3.
Listing 17.3. Using parentheses in macros.
1: // Listing 17.3 Macro Expansion 2: #include <iostream.h> 3: 4: #define CUBE(a) ( (a) * (a) * (a) ) 5: #define THREE(a) a * a * a 6: 7: int main() 8: { 9: long x = 5; 10: long y = CUBE(x); 11: long z = THREE(x); 12: 13: cout << "y: " << y << endl; 14: cout << "z: " << z << endl; 15: 16: long a = 5, b = 7; 17: y = CUBE(a+b); 18: z = THREE(a+b); 19: 20: cout << "y: " << y << endl; 21: cout << "z: " << z << endl; 22: return 0; 23: } Output: y: 125 z: 125 y: 1728 z: 82Analysis: On line 4, the macro CUBE is defined, with the argument x put into parentheses each time it is used. On line 5, the macro THREE is defined, without the parentheses.
In the second use, on lines 16-18, the parameter is 5 + 7. In this case, CUBE(5+7) evaluates to
( (5+7) * (5+7) * (5+7) )which evaluates to
( (12) * (12) * (12) )which in turn evaluates to 1728. THREE(5+7), however, evaluates to
5 + 7 * 5 + 7 * 5 + 7Because multiplication has a higher precedence than addition, this becomes
5 + (7 * 5) + (7 * 5) + 7which evaluates to
5 + (35) + (35) + 7which finally evaluates to 82.
The second problem is that macros are expanded inline each time they are used. This means that if a macro is used a dozen times, the substitution will appear 12 times in your program, rather than appear once as a function call will. On the other hand, they are usually quicker than a function call because the overhead of a function call is avoided.
The fact that they are expanded inline leads to the third problem, which is that the macro does not appear in the intermediate source code used by the compiler, and therefore is unavailable in most debuggers. This makes debugging macros tricky.
The final problem, however, is the biggest: macros are not type-safe. While it is convenient that absolutely any argument may be used with a macro, this completely undermines the strong typing of C++ and so is anathema to C++ programmers. However, there is a way to overcome this problem, as you'll see on Day 19, "Templates."
Listing 17.4. Using inline rather than a macro.
1: #include <iostream.h> 2: 3: inline unsigned long Square(unsigned long a) { return a * a; } 4: inline unsigned long Cube(unsigned long a) 5: { return a * a * a; } 6: int main() 7: { 8: unsigned long x=1 ; 9: for (;;) 10: { 11: cout << "Enter a number (0 to quit): "; 12: cin >> x; 13: if (x == 0) 14: break; 15: cout << "You entered: " << x; 16: cout << ". Square(" << x << "): "; 17: cout << Square(x); 18: cout<< ". Cube(" _<< x << "): "; 19: cout << Cube(x) << "." << endl; 20: } 21: return 0; 22: } Output: Enter a number (0 to quit): 1 You entered: 1. Square(1): 1. Cube(1): 1. Enter a number (0 to quit): 2 You entered: 2. Square(2): 4. Cube(2): 8. Enter a number (0 to quit): 3 You entered: 3. Square(3): 9. Cube(3): 27. Enter a number (0 to quit): 4 You entered: 4. Square(4): 16. Cube(4): 64. Enter a number (0 to quit): 5 You entered: 5. Square(5): 25. Cube(5): 125. Enter a number (0 to quit): 6 You entered: 6. Square(6): 36. Cube(6): 216. Enter a number (0 to quit): 0Analysis: On lines 3 and 4, two inline functions are declared: Square() and Cube(). Each is declared to be inline, so like a macro function these will be expanded in place for each call, and there will be no function call overhead.
On line 16, the function Square is called, as is the function Cube. Again, because these are inline functions, it is exactly as if this line had been written like this:
16: cout << ". Square(" << x << "): " << x * x << ". Cube(" << x << Â"): " << x * x * x << "." << endl;
#define WRITESTRING(x) cout << #xand then call
WRITESTRING(This is a string);the precompiler will turn it into
cout << "This is a string";Note that the string This is a string is put into quotes, as required by cout.
Assume for a moment that you have five functions, named fOnePrint, fTwoPrint, fThreePrint, fFourPrint, and fFivePrint. You can then declare:
#define fPRINT(x) f ## x ## Printand then use it with fPRINT(Two) to generate fTwoPrint and with fPRINT(Three) to generate fThreePrint.
At the conclusion of Week 2, a PartsList class was developed. This list could only handle objects of type List. Let's say that this list works well, and you'd like to be able to make lists of animals, cars, computers, and so forth.
One approach would be to create AnimalList, CarList, ComputerList, and so on, cutting and pasting the code in place. This will quickly become a nightmare, as every change to one list must be written to all the others.
An alternative is to use macros and the concatenation operator. For example, you could define
#define Listof(Type) class Type##List \ { \ public: \ Type##List(){} \ private: \ int itsLength; \ };This example is overly sparse, but the idea would be to put in all the necessary methods and data. When you were ready to create an AnimalList, you would write
Listof(Animal)and this would be turned into the declaration of the AnimalList class. There are some problems with this approach, all of which are discussed in detail on Day 19, when templates are discussed.
When the precompiler sees one of these macros, it makes the appropriate substitutes. For __DATE__, the current date is substituted. For __TIME__, the current time is substituted. __LINE__ and __FILE__ are replaced with the source code line number and filename, respectively. You should note that this substitution is made when the source is precompiled, not when the program is run. If you ask the program to print __DATE__, you will not get the current date; instead, you will get the date the program was compiled. These defined macros are very useful in debugging.
One powerful feature of the assert() macro is that the preprocessor collapses it into no code at all if DEBUG is not defined. It is a great help during development, and when the final product ships there is no performance penalty nor increase in the size of the executable version of the program.
Rather than depending on the compiler-provided assert(), you are free to write your own assert() macro. Listing 17.5 provides a simple assert() macro and shows its use.
Listing 17.5. A simple assert() macro.
1: // Listing 17.5 ASSERTS 2: #define DEBUG 3: #include <iostream.h> 4: 5: #ifndef DEBUG 6: #define ASSERT(x) 7: #else 8: #define ASSERT(x) \ 9: if (! (x)) \ 10: { \ 11: cout << "ERROR!! Assert " << #x << " failed\n"; \ 12: cout << " on line " << __LINE__ << "\n"; \ 13: cout << " in file " << __FILE__ << "\n"; \ 14: } 15: #endif 16: 17: 18: int main() 19: { 20: int x = 5; 21: cout << "First assert: \n"; 22: ASSERT(x==5); 23: cout << "\nSecond assert: \n"; 24: ASSERT(x != 5); 25: cout << "\nDone.\n"; 26: return 0; 27: } Output: First assert: Second assert: ERROR!! Assert x !=5 failed on line 24 in file test1704.cpp Done.Analysis: On line 2, the term DEBUG is defined. Typically, this would be done from the command line (or the IDE) at compile time, so you can turn this on and off at will. On lines 8-14, the assert() macro is defined. Typically, this would be done in a header file, and that header (ASSERT.HPP) would be included in all your implementation files.
On line 5, the term DEBUG is tested. If it is not defined, assert() is defined to create no code at all. If DEBUG is defined, the functionality defined on lines 8-14 is applied.
The assert() itself is one long statement, split across seven source code lines, as far as the precompiler is concerned. On line 9, the value passed in as a parameter is tested; if it evaluates FALSE, the statements on lines 11-13 are invoked, printing an error message. If the value passed in evaluates TRUE, no action is taken.
There is no penalty for frequent use of assert(); it is removed from the code when you undefine debugging. It also provides good internal documentation, reminding the reader of what you believe is true at any given moment in the flow of the code.
This is critical, because when you ship your code to your customers, instances of assert() will be removed. You can't depend on an assert() to handle a runtime problem, because the assert() won't be there.
It is a common mistake to use assert() to test the return value from a memory assignment:
Animal *pCat = new Cat; Assert(pCat); // bad use of assert pCat->SomeFunction();This is a classic programming error; every time the programmer runs the program, there is enough memory and the assert() never fires. After all, the programmer is running with lots of extra RAM to speed up the compiler, debugger, and so forth. The programmer then ships the executable, and the poor user, who has less memory, reaches this part of the program and the call to new fails and returns NULL. The assert(), however, is no longer in the code and there is nothing to indicate that the pointer points to NULL. As soon as the statement pCat->SomeFunction() is reached, the program crashes.
Getting NULL back from a memory assignment is not a programming error, although it is an exceptional situation. Your program must be able to recover from this condition, if only by throwing an exception. Remember: The entire assert() statement is gone when DEBUG is undefined. Exceptions are covered in detail on Day 20.
ASSERT (x = 5)when you mean to test whether x == 5, you will create a particularly nasty bug.
Let's say that just prior to this assert() you called a function that set x equal to 0. With this assert() you think you are testing whether x is equal to 5; in fact, you are setting x equal to 5. The test returns TRUE, because x = 5 not only sets x to 5, but returns the value 5, and because 5 is non-zero it evaluates as TRUE.
Once you pass the assert() statement, x really is equal to 5 (you just set it!). Your program runs just fine. You're ready to ship it, so you turn off debugging. Now the assert() disappears, and you are no longer setting x to 5. Because x was set to 0 just before this, it remains at 0 and your program breaks.
In frustration, you turn debugging back on, but hey! Presto! The bug is gone. Once again, this is rather funny to watch, but not to live through, so be very careful about side effects in debugging code. If you see a bug that only appears when debugging is turned off, take a look at your debugging code with an eye out for nasty side effects.
It can be very helpful to declare an Invariants() method that returns TRUE only if each of these conditions is still true. You can then ASSERT(Invariants()) at the start and completion of every class method. The exception would be that your Invariants() would not expect to return TRUE before your constructor runs or after your destructor ends. Listing 17.6 demonstrates the use of the Invariants() method in a trivial class.
Listing 17.6. Using Invariants().
0: #define DEBUG 1: #define SHOW_INVARIANTS 2: #include <iostream.h> 3: #include <string.h> 4: 5: #ifndef DEBUG 6: #define ASSERT(x) 7: #else 8: #define ASSERT(x) \ 9: if (! (x)) \ 10: { \ 11: cout << "ERROR!! Assert " << #x << " failed\n"; \ 12: cout << " on line " << __LINE__ << "\n"; \ 13: cout << " in file " << __FILE__ << "\n"; \ 14: } 15: #endif 16: 17: 18: const int FALSE = 0; 19: const int TRUE = 1; 20: typedef int BOOL; 21: 22: 23: class String 24: { 25: public: 26: // constructors 27: String(); 28: String(const char *const); 29: String(const String &); 30: ~String(); 31: 32: char & operator[](int offset); 33: char operator[](int offset) const; 34: 35: String & operator= (const String &); 36: int GetLen()const { return itsLen; } 37: const char * GetString() const { return itsString; } 38: BOOL Invariants() const; 39: 40: private: 41: String (int); // private constructor 42: char * itsString; 43: // unsigned short itsLen; 44: int itsLen; 45: }; 46: 47: // default constructor creates string of 0 bytes 48: String::String() 49: { 50: itsString = new char[1]; 51: itsString[0] = `\0'; 52: itsLen=0; 53: ASSERT(Invariants()); 54: } 55: 56: // private (helper) constructor, used only by 57: // class methods for creating a new string of 58: // required size. Null filled. 59: String::String(int len) 60: { 61: itsString = new char[len+1]; 62: for (int i = 0; i<=len; i++) 63: itsString[i] = `\0'; 64: itsLen=len; 65: ASSERT(Invariants()); 66: } 67: 68: // Converts a character array to a String 69: String::String(const char * const cString) 70: { 71: itsLen = strlen(cString); 72: itsString = new char[itsLen+1]; 73: for (int i = 0; i<itsLen; i++) 74: itsString[i] = cString[i]; 75: itsString[itsLen]='\0'; 76: ASSERT(Invariants()); 77: } 78: 79: // copy constructor 80: String::String (const String & rhs) 81: { 82: itsLen=rhs.GetLen(); 83: itsString = new char[itsLen+1]; 84: for (int i = 0; i<itsLen;i++) 85: itsString[i] = rhs[i]; 86: itsString[itsLen] = `\0'; 87: ASSERT(Invariants()); 88: } 89: 90: // destructor, frees allocated memory 91: String::~String () 92: { 93: ASSERT(Invariants()); 94: delete [] itsString; 95: itsLen = 0; 96: } 97: 98: // operator equals, frees existing memory 99: // then copies string and size 100: String& String::operator=(const String & rhs) 101: { 102: ASSERT(Invariants()); 103: if (this == &rhs) 104: return *this; 105: delete [] itsString; 106: itsLen=rhs.GetLen(); 107: itsString = new char[itsLen+1]; 108: for (int i = 0; i<itsLen;i++) 109: itsString[i] = rhs[i]; 110: itsString[itsLen] = `\0'; 111: ASSERT(Invariants()); 112: return *this; 113: } 114: 115: //non constant offset operator, returns 116: // reference to character so it can be 117: // changed! 118: char & String::operator[](int offset) 119: { 120: ASSERT(Invariants()); 121: if (offset > itsLen) 122: return itsString[itsLen-1]; 123: else 124: return itsString[offset]; 125: ASSERT(Invariants()); 126: } 127: 128: // constant offset operator for use 129: // on const objects (see copy constructor!) 130: char String::operator[](int offset) const 131: { 132: ASSERT(Invariants()); 133: if (offset > itsLen) 134: return itsString[itsLen-1]; 135: else 136: return itsString[offset]; 137: ASSERT(Invariants()); 138: } 139: 140: 141: BOOL String::Invariants() const 142: { 143: #ifdef SHOW_INVARIANTS 144: cout << " String OK "; 145: #endif 146: return ( (itsLen && itsString) || 147: (!itsLen && !itsString) ); 148: } 149: 150: class Animal 151: { 152: public: 153: Animal():itsAge(1),itsName("John Q. Animal") 154: {ASSERT(Invariants());} 155: Animal(int, const String&); 156: ~Animal(){} 157: int GetAge() { ASSERT(Invariants()); return itsAge;} 158: void SetAge(int Age) 159: { 160: ASSERT(Invariants()); 161: itsAge = Age; 162: ASSERT(Invariants()); 163: } 164: String& GetName() 165: { 166: ASSERT(Invariants()); 167: return itsName; 168: } 169: void SetName(const String& name) 170: { 171: ASSERT(Invariants()); 172: itsName = name; 173: ASSERT(Invariants()); 174: } 175: BOOL Invariants(); 176: private: 177: int itsAge; 178: String itsName; 179: }; 180: 181: Animal::Animal(int age, const String& name): 182: itsAge(age), 183: itsName(name) 184: { 185: ASSERT(Invariants()); 186: } 187: 188: BOOL Animal::Invariants() 189: { 190: #ifdef SHOW_INVARIANTS 191: cout << " Animal OK "; 192: #endif 193: return (itsAge > 0 && itsName.GetLen()); 194: } 195: 196: int main() 197: { 198: Animal sparky(5,"Sparky"); 199: cout << "\n" << sparky.GetName().GetString() << " is "; 200: cout << sparky.GetAge() << " years old."; 201: sparky.SetAge(8); 202: cout << "\n" << sparky.GetName().GetString() << " is "; 203: cout << sparky.GetAge() << " years old."; 204: return 0; 205: } Output: String OK String OK String OK String OK String OK String OK String OK Animal OK String OK Animal OK Sparky is Animal OK 5 years old. Animal OK Animal OK Animal OK Sparky is Animal OK 8 years old. String OKAnalysis: On lines 6-16, the assert() macro is defined. If DEBUG is defined, this will write out an error message when the assert() macro evaluates FALSE.
This pattern is repeated for the other constructors, and the destructor calls Invariants() only before it sets out to destroy the object. The remaining class functions call Invariants() both before taking any action and then again before returning. This both affirms and validates a fundamental principal of C++: Member functions other than constructors and destructors should work on valid objects and should leave them in a valid state.
On line 175, class Animal declares its own Invariants() method, implemented on lines 188-194. Note on lines 154, 157, 160, and 162 that inline functions can call the Invariants() method.
Listing 17.7. Printing values in DEBUG mode.
1: // Listing 17.7 - Printing values in DEBUG mode 2: #include <iostream.h> 3: #define DEBUG 4: 5: #ifndef DEBUG 6: #define PRINT(x) 7: #else 8: #define PRINT(x) \ 9: cout << #x << ":\t" << x << endl; 10: #endif 11: 12: enum BOOL { FALSE, TRUE } ; 13: 14: int main() 15: { 16: int x = 5; 17: long y = 73898l; 18: PRINT(x); 19: for (int i = 0; i < x; i++) 20: { 21: PRINT(i); 22: } 23: 24: PRINT (y); 25: PRINT("Hi."); 26: int *px = &x; 27: PRINT(px); 28: PRINT (*px); 29: return 0; 30: } Output: x: 5 i: 0 i: 1 i: 2 i: 3 i: 4 y: 73898 "Hi.": Hi. px: 0x2100 (You may receive a value other than 0x2100) *px: 5Analysis: The macro on lines 5-10 provides printing of the current value of the supplied parameter. Note that the first thing fed to cout is the stringized version of the parameter; that is, if you pass in x, cout receives "x".
Next, cout receives the quoted string ":\t", which prints a colon and then a tab. Third, cout receives the value of the parameter (x), and then finally, endl, which writes a new line and flushes the buffer.
To define a level, simply follow the #define DEBUG statement with a number. While you can have any number of levels, a common system is to have four levels: HIGH, MEDIUM, LOW, and NONE. Listing 17.8 illustrates how this might be done, using the String and Animal classes from Listing 17.6. The definitions of the class methods other than Invariants() have been left out to save space because they are unchanged from Listing 17.6.
Listing 17.8. Levels of debugging.
NOTE: To compile this code, copy lines 43-136 of Listing 17.6 between lines 64 and 65 of this listing.
0: enum LEVEL { NONE, LOW, MEDIUM, HIGH }; 1: const int FALSE = 0; 2: const int TRUE = 1; 3: typedef int BOOL; 4: 5: #define DEBUGLEVEL HIGH 6: 7: #include <iostream.h> 8: #include <string.h> 9: 10: #if DEBUGLEVEL < LOW // must be medium or high 11: #define ASSERT(x) 12: #else 13: #define ASSERT(x) \ 14: if (! (x)) \ 15: { \ 16: cout << "ERROR!! Assert " << #x << " failed\n"; \ 17: cout << " on line " << __LINE__ << "\n"; \ 18: cout << " in file " << __FILE__ << "\n"; \ 19: } 20: #endif 21: 22: #if DEBUGLEVEL < MEDIUM 23: #define EVAL(x) 24: #else 25: #define EVAL(x) \ 26: cout << #x << ":\t" << x << endl; 27: #endif 28: 29: #if DEBUGLEVEL < HIGH 30: #define PRINT(x) 31: #else 32: #define PRINT(x) \ 33: cout << x << endl; 34: #endif 35: 36: 37: class String 38: { 39: public: 40: // constructors 41: String(); 42: String(const char *const); 43: String(const String &); 44: ~String(); 45: 46: char & operator[](int offset); 47: char operator[](int offset) const; 48: 49: String & operator= (const String &); 50: int GetLen()const { return itsLen; } 51: const char * GetString() const 52: { return itsString; } 53: BOOL Invariants() const; 54: 55: private: 56: String (int); // private constructor 57: char * itsString; 58: unsigned short itsLen; 59: }; 60: 61: BOOL String::Invariants() const 62: { 63: PRINT("(String Invariants Checked)"); 64: return ( (BOOL) (itsLen && itsString) || 65: (!itsLen && !itsString) ); 66: } 67: 68: class Animal 69: { 70: public: 71: Animal():itsAge(1),itsName("John Q. Animal") 72: {ASSERT(Invariants());} 73: 74: Animal(int, const String&); 75: ~Animal(){} 76: 77: int GetAge() 78: { 79: ASSERT(Invariants()); 80: return itsAge; 81: } 82: 83: void SetAge(int Age) 84: { 85: ASSERT(Invariants()); 86: itsAge = Age; 87: ASSERT(Invariants()); 88: } 89: String& GetName() 90: { 91: ASSERT(Invariants()); 92: return itsName; 93: } 94: 95: void SetName(const String& name) 96: { 97: ASSERT(Invariants()); 98: itsName = name; 99: ASSERT(Invariants()); 100: } 101: 102: BOOL Invariants(); 103: private: 104: int itsAge; 105: String itsName; 106: }; 107: 108: BOOL Animal::Invariants() 109: { 110: PRINT("(Animal Invariants Checked)"); 111: return (itsAge > 0 && itsName.GetLen()); 112: } 113: 114: int main() 115: { 116: const int AGE = 5; 117: EVAL(AGE); 118: Animal sparky(AGE,"Sparky"); 119: cout << "\n" << sparky.GetName().GetString(); 120: cout << " is "; 121: cout << sparky.GetAge() << " years old."; 122: sparky.SetAge(8); 123: cout << "\n" << sparky.GetName().GetString(); 124: cout << " is "; 125: cout << sparky.GetAge() << " years old."; 126: return 0; 127: } Output: AGE: 5 (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) (String Invariants Checked) Sparky is (Animal Invariants Checked) 5 Years old. (Animal Invariants Checked) (Animal Invariants Checked) (Animal Invariants Checked) Sparky is (Animal Invariants Checked) 8 years old. (String Invariants Checked) (String Invariants Checked) // run again with DEBUG = MEDIUM AGE: 5 Sparky is 5 years old. Sparky is 8 years old.Analysis: On lines 10 to 20, the assert() macro is defined to be stripped if DEBUGLEVEL is less than LOW (that is, DEBUGLEVEL is NONE). If any debugging is enabled, the assert() macro will work. On line 23, EVAL is declared to be stripped if DEBUG is less than MEDIUM; if DEBUGLEVEL is NONE or LOW, EVAL is stripped.
PRINT is used within the Invariants() methods to print an informative message. EVAL is used on line 117 to evaluate the current value of the constant integer AGE.
DO use CAPITALS for your macro names. This is a pervasive convention, and other programmers will be confused if you don't. DON'T allow your macros to have side effects. Don't increment variables or assign values from within a macro. DO surround all arguments with parentheses in macro functions.
The preprocessor does text substitution, although with the use of macros these can be somewhat complex. By using #ifdef, #else, and #ifndef, you can accomplish conditional compilation, compiling in some statements under one set of conditions and in another set of statements under other conditions. This can assist in writing programs for more than one platform and is often used to conditionally include debugging information.
Macro functions provide complex text substitution based on arguments passed at compile time to the macro. It is important to put parentheses around every argument in the macro to ensure the correct substitution takes place.
Macro functions, and the preprocessor in general, are less important in C++ than they were in C. C++ provides a number of language features, such as const variables and templates, that offer superior alternatives to use of the preprocessor.
A. First, C++ is backward-compatible with C, and all significant
parts of C must be supported in C++. Second, there are some uses of the
preprocessor that are still used frequently in C++, such as inclusion guards.
Q. Why use macro functions when you can use a regular function?
A. Macro functions are expanded inline and are used as a substitute for repeatedly typing the same commands with minor variations. Again, though, templates offer a better alternative.
Q. How do you know when to use a macro versus an inline function?
A. Often it doesn't matter much; use whichever is simpler. However, macros offer character substitution, stringizing, and concatenation. None of these is available with functions.
Q. What is the alternative to using the preprocessor to print interim values during debugging?
A. The best alternative is to use watch statements within a debugger. For information on watch statements, consult your compiler or debugger documentation.
Q. How do you decide when to use an assert() and when to throw an exception?
A. If the situation you're testing can be true without your having committed a programming error, use an exception. If the only reason for this situation to ever be true is a bug in your program, use an assert().
2. How do you instruct your compiler to print the contents
of the intermediate file showing the effects of the preprocessor?
3. What is the difference between #define debug 0 and #undef debug?
4. Name four predefined macros.
5. Why can't you call Invariants() as the first line of your constructor?
2. Write an assert() macro that prints an error
message and the file and line number if debug level is 2, just a message
(without file and line number) if the level is 1, and does nothing if the
level is 0.
3. Write a macro DPrint that tests if DEBUG is defined and, if it is, prints the value passed in as a parameter.
4. Write a function that prints an error message. The function should print the line number and filename where the error occurred. Note that the line number and filename are passed in to this function.
5. How would you call the preceding error function?
6. Write an assert() macro that uses the error function from Exercise 4, and write a driver program that calls this assert() macro.