Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ Timesaving Techniques (2005) [eng].pdf
Скачиваний:
65
Добавлен:
16.08.2013
Размер:
8.35 Mб
Скачать

Delayed Construction

27

Testing the

ConfigurationFile Class

After you create a class, create a test driver that not only ensures that your code is correct, but also shows people how to use your code.

1. In the code editor of your choice, reopen the source file to hold the code for your test program.

In this example, I named the test program ch04.cpp. You could, of course, call this program anything you wanted, since filenames are only human-readable strings. The compiler does not care what you call your file.

2. Type the code from Listing 4-4 into your file.

Or better yet, copy the code from the source file on this book’s companion Web site.

LISTING 4-4: THE CONFIGURATIONFILE TEST PROGRAM

int main(int argc, char **argv)

{

ConfigurationFile cf(“test.dat”); cf.AddProperty( “Name”, “Matt” ); cf.AddProperty( “Address”, “1000 Main St” );

}

3. Save the code in the source-code file created in your editor, and then close the editor application.

4. Compile the source code with your favorite compiler on your favorite operating system.

5. Run the program on your favorite operatingsystem console.

If you’ve done everything properly, you should see the following output from the program on the console window:

$ ./a.exe

$ cat test.dat Name=Matt Address=1000 Main St

As you can see, the configuration file was properly saved to the output file.

Delayed Construction

Although the constructor for a class is all wonderful and good, it does bring up an interesting point. What if something goes wrong in the construction process and you need to signal the user? You have two ways to approach this situation; both have their positives and negatives:

You can throw an exception. In general, however, I wouldn’t. Throwing exceptions is an option I discuss later, in Technique 53 — but doing so is rarely a good idea. Your users are really not expecting a constructor to throw an exception. Worse, an exception might leave the object in some ambiguous state, where it’s unclear whether the constructor has finished running. If you do choose this route, you should also make sure that all values are initialized before you do anything that might generate an exception. (For example, what happens if you throw an exception in a base-class constructor? The error would be propagated up to the main program. This would be very confusing to the user, who wouldn’t even know where the error was coming from.)

You can delay any work that might create an error until later in the processing of the object.

This option is usually more valuable and is worth further exploration.

Let’s say, for example, that you are going to open a file in your constructor. The file-opening process could certainly fail, for any number of reasons. One way to handle this error is to check for it, but this might be confusing to the end user, because they

28 Technique 4: Inheriting Data and Functionality

would not understand where the file was being opened in the first place and why it failed to open properly. In cases like this, instead of a constructor that looks like this . . .

FileOpener::FileOpener( const char *strFileName)

{

fpIn = fopen(strFileName, “r”);

}

. . . you might instead choose to do the following:

FileOpener::FileOpener( const char *strFileName)

{

 

 

 

// Hold onto the file name for later

 

use.

 

 

 

sFileName

= strFileName;

 

 

bIsOpen

= false;

 

 

}

 

 

5

bool FileOpener::OpenFile()

 

{

 

 

if ( !bIsOpen )

{

fpIn = fopen(sFileName.c_str(), “r”);

if ( fpIn != NULL ) bIsOpen = true;

}

return bIsOpen;

}

Because we cannot return an error from a constructor directly, we break the process into two pieces. The first piece assigns the member variables to the values that the user passed in. There is no way that an error can occur in this process, so the object will be properly constructed. In the OpenFile method ( 5 in the above listing), we then try to open the file, and indicate the status as the return value of the method.

Then, when you tell your code to actually read from the file, you would do something like this:

bool FileOpener::SomeMethod()

{

if ( OpenFile() )

{

// Continue with processing

}

else

// Generate an error return false;

}

The advantage to this approach is that you can wait until you absolutely have to before you actually open the file that the class operates on. Doing so means you don’t have file overhead every time you construct an object — and you don’t have to worry about closing the darn thing if it was never opened. The advantage of delaying the construction is that you can wait until the data is actually needed before doing the time and memory expensive operation of file input and output.

With a little closer look back at the SavePairs class (Listing 4-2), you can see a very serious error lurking there. (Just for practice, take a moment to go back over the class and look for what’s missing.)

Do you see it? Imagine that you have an object of type SavePairs, for an example. Now you can make a copy of that object by assigning it to another object of the SavePairs class, or by passing it by value into a method like this:

DoSave( SavePairs obj );

When you make the above function call, you are making a copy of the obj object by invoking the copy constructor for the class. Now, because you didn’t create a copy constructor, you have a serious problem. Why? A copy is a bitwise copy of all elements in the class. When a copy is made of the FILE pointer in the class, it means you now have two pointers pointing to the same block of memory. Uh-oh. Because you will destroy that memory in the destructor for the class (by calling fclose), the code frees up the same block of memory twice. This is a classic problem that you need to solve whenever you are allocating memory in a class. In this case, you really want to be able to copy the pointer without closing it in the copy. So, what you really need to do is keep track of whether the pointer in question is a copy or an original. To do so, you could rewrite the class as in Listing 4-5:

Delayed Construction

29

LISTING 4-5: THE REVISED SAVEPAIRS CLASS

class SavePairs

{

FILE *fpIn; bool bIsACopy;

public:

SavePairs( void )

{

fpIn = NULL; bIsACopy = false;

}

SavePairs( const char *strName )

{

fpIn = fopen( strName, “w” ); bIsACopy = false;

}

SavePairs( const SavePairs& aCopy )

{

fpIn = aCopy.fpIn; bIsACopy = true;

}

virtual ~SavePairs()

{

if ( fpIn && !bIsACopy ) fclose(fpIn);

}

void SaveAPair( std::string name, std::string value )

{

if ( fpIn )

fprintf(fpIn, “%s=%s\n”, name.c_str(), value.c_str());

}

};

This code in Listing 4-5 has the advantage of working correctly no matter how it is handled. If you pass a pointer into the file, the code will make a copy of it and not delete it. If you use the original of the file pointer, it will be properly deleted, not duplicated.

This is an improvement. But does this code really fix all possible problems? The answer, of course, is no. Imagine the following scenario:

1. Create a SavePairs object.

2. Copy the object by calling the copy constructor with a new object.

3. Delete the original SavePairs object.

4. Invoke a method on the copy that uses the file pointer.

What happens in this scenario? Nothing good, you can be sure. The problem occurs when the last step is hit, and the copied file pointer is used. The original pointer has been deleted, so the copy is pointing at junk. Bad things happen — and your program likely crashes.

A joke that runs around the Internet compares various programming languages in terms of shooting yourself in the foot. The entire joke is easy enough to find, but the part that applies to this subject looks something like this:

C: You shoot yourself in the foot.

C++: You accidentally create a dozen instances of yourself and shoot them all in the foot. Providing emergency assistance is impossible because you can’t tell which instances are bitwise copies and which are just pointing at others, saying, “That’s me over there.”

Many programmers find the joke is too true to be amusing. C++ gives you the (metaphorical) ability to blow off your foot any time you try to compile. There are so many things to think about, and so many possibilities to consider.

The best way to avoid the disasters of the past is to plan for them in the future. This is nowhere more true than when you’re working with the basic building blocks of the system, constructors and destructors. If you do not do the proper groundwork to make sure that your class is as safe as possible, you will pay for it in the long run — each and every time. Make sure that you always implement virtual destructors and check for all copies of your objects in your code. Doing so will make your code cleaner (dare I say “bulletproof”?) and eliminate problems of this sort that would otherwise naturally crop up later.

5 Separating Rules

and Data from Code

Technique

Save Time By

Using encapsulation to separate rules and data from code

Building a datavalidation class

Testing the datavalidation class

One of the biggest problems in the software-development world is maintaining code that we did not design or implement in the first place. Often the hardest thing to do in such cases is to figure out exactly how the code was meant to work. Usually, there is copious documentation that tells you what the code is doing (or what the original pro-

grammer thought was going on), but very rarely does it tell you why.

The reason is that the business rules and the data that implement those rules are usually embedded somewhere in the code. Hard-coded dates, values — even user names and passwords — can be hidden deep inside the code base. Wouldn’t it be nice if there was some way to extract all of that data and those business rules and put them in one place? This really does sound like a case for encapsulation, now doesn’t it? Of course it does. As I discuss in Technique 1, encapsulation allows us to insulate the user from the implementation of things. That statement is ambiguous and means quite a few things, so to clarify, let me show you a couple of examples. First, consider the case of the business rule.

When you are creating code for a software project, you must often consider rules that apply across the entire business — such as the allowable number of departments in an accounting database, or perhaps a calculation to determine the maximum amount of a pay raise for a given employee. These rules appear in the form of code snippets scattered across the entire project, residing in different files and forms. When the next project comes along, they are often duplicated, modified, or abandoned. The problem with this approach is that it becomes harder and harder to keep track of what the rules are and what they mean.

Assume, for the moment, that you have to implement some code that checks for dates in the system. (Okay, a date isn’t strictly a business rule per se, but it makes a handy example.) To run the check, you could try scattering some code around the entire system to check for leap years, date validity, and so forth, but that would be inefficient and wasteful. Here’s why that solution is no solution at all: