Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

You Can Program In C++ (2006) [eng]

.pdf
Скачиваний:
80
Добавлен:
16.08.2013
Размер:
4.33 Mб
Скачать

268

CHAPTER 14

The above definitions are not robust, production-quality ones. For example, if there are not two digits in data giving the position of the piece, get position( ) will fail, possibly catastrophically. I am leaving it as an exercise for the reader to provide data validation coupled with appropriate action (such as throwing an exception for a corrupt input file).

Language Note: C programmers should note that the Standard C headers from the C90 standard are valid in C++. However, the preferred option in C++ is to use headers without the .h extension but prefixing the C header name with the letter c. Therefore, for example, the C header <string.h> becomes <cstring> in C++. That particular header is subject to confusion in C++ because <string> is the C++ header in which std::string is declared, while <cstring> provides the declarations of C’s <string.h> (but encapsulated in namespace std).

Converting Text to an Enumerator

Our major problem with get piece( ) is that we store the data in a human-readable form, where the name of a piece is saved as text. However, we are representing pieces by enumerators of the chesspiece::piece enum type. The problem is how to convert from the external textual representation to the internal enumerator. We could write something such as:

chesspiece::piece get_enumumerator(std::string const & textname){ if(textname == "pawn") return chesspiece::pawn;

if(textname == "rook") return chesspiece::rook; // and so on

}

That is a perfectly valid option. However, it lacks elegance, and such a method can cause problems with maintenance. Ideally, we would like to be able to look up the name in a suitable table and get the enumerator straight back. The C++ std::map container (declared in <map>) provides a suitable data structure for this purpose. A map consists of a set of key–value pairs. Like all the Standard Library containers, std::map is a template that can be used for any suitable combination of types for the key and value. In this case, we define:

std::map<std::string, chesspiece::piece> text_to_enum;

We need a function to initialize the table. We also need to decide where we are going to declare the table. As this is to do with our chess-piece concept, I think that both text to enum and the function that initializes the table should be static members of chesspiece.

Here is a suitable initialization function:

void chesspiece::init_text_to_enum( ){ text_to_enum["bishop"] = chesspiece::bishop; text_to_enum["king"] = chesspiece::king; text_to_enum["knight"] = chesspiece::knight; text_to_enum["pawn"] = chesspiece::pawn; text_to_enum["queen"] = chesspiece::queen; text_to_enum["indeterminate"] = chesspiece::indeterminate;

}

That may look strange to you unless you have come across such containers before. We can access a std::map object by using a key as a subscript. If the key is already in the map, the corresponding value will be used exactly as if it were a variable. If the key is not found, it will be automatically added to the map. chesspiece::init text to enum( ) effectively inserts six text keys as instances of

std::string and pairs them with the corresponding chesspiece::piece enumerators. That function must be executed at least once (executing it more than once has no effect other than taking time) before statements such as

STREAMS, FILES, AND PERSISTENCE

269

std::cout << text_to_enum["knight"] << " is the code for a knight.\n";

can be executed correctly. Otherwise, the program will compile and execute, but give incorrect output.

T R Y T H I S

Add declarations of text to enum and init text to enum( ) as static members of chesspiece. Add the definitions to the implementation file for chesspiece (remember that you have to provide the definitions for static data members). Now build and execute the following:

int main( ){ chesspiece::init_text_to_enum( );

std::cout << "The internal representation of a knight is " << chesspiece::text_to_enum["knight"] << ".\n";

}

Now we are ready to provide a definition of the function to extract the type of chess piece from our std::stringstream object holding the specification of a piece:

chesspiece::piece get_piece_type(std::stringstream & data){ std::string pce;

data >> pce;

return chesspiece::text_to_enum[pce];

}

This is not a robust definition but one that assumes that the data includes the correctly spelled name of a chess piece.

EXERCISES

5.Create a project, and add the declarations of all the functions necessary for implementing create piece( ) to a header file. Put the corresponding definitions in an implementation file. The following small program will act as an initial test:

int main( ){ std::stringstream source;

source << "white pawn at (1, 2)"; chessboard board; create_piece(source, board);

}

6.Design and implement a function that saves the specification of a piece to a file. Test it, and try using the file as a source of data for Exercise 5.

270CHAPTER 14

7.Reorganize your code base so that create piece( ) and the matching save piece( ) become implementation details of chessboard. Note that the reorganization should include moving as much of the implementation as possible into the appropriate implementation file, and into that file’s unnamed namespace.

8.Add save board( ) and get board( ) as members of chessboard, so that you can save a board position to a file and then recover it from that file.

9.Add a constructor to chessboard that loads a position from a stream. Change its destructor so that it offers to save the position before destroying the board.

S T R E T C H I N G E X E R C I S E

10.Enhance the deck class so that you can write the cards out to storage and then recover them. The storage should be in human-readable text.

REFERENCE SECTION

Streams

C++ provides several major categories of stream class. Each of them provides facilities for formatting data as well as sending character data to a sink or extracting it from a source. Most of these facilities are provided through templates so that the user can select the character type (by default, either char or wchar t). Streams default to handling data as text, though they can be constructed to handle binary data (no conversion to and from text).

Streams are based on stream-buffer classes (which handle the actual input and output operations). This book does not go into the details of those classes.

The high-level programmer is concerned with three types of stream: the standard console streams, the file streams, and the string streams. Each of these has versions based on char and wchar t, as well as facilities for extending them to other kinds of character. There is a fourth, legacy stream type that uses an array of char as its input/output buffer. This should not normally be used in new code, and I am not providing details of it in this book.

In general, whenever a stream operation fails, the instance is set into some kind of dormant state where all subsequent attempts to use it do nothing until the program resets the stream object by applying the clear( ) member function. A stream in any form of dormant state evaluates as false in any context where a bool value is expected.

A stream can be in one of four states:

fail: Some operation since the last time it was set to a good state has failed. This condition can be detected by applying the fail( ) member function to the stream object. That returns true if the object is in a fail state. For example, an attempt to extract an int value from std::cin when the next item of data does not represent such a value places std::cin into a fail state. All subsequent

STREAMS, FILES, AND PERSISTENCE

271

attempts to extract data from std::cin will be ignored (i.e. do nothing) until the stream is reset with std::cin.clear( ).

bad: Some operation has failed in a way that may involve the loss of data or the corruption of the stream. This is a more severe situation than those flagged by the fail state. Writing data to a full disk might cause a file stream to enter such a state. The bad( ) member function returns true if the object is in a bad state.

eof: An end-of-file marker has been read. (This includes input of an end-of-file marker from the keyboard, though what constitutes such a marker depends on the operating system. Ctrl+Z is an EOF for Windows systems; Ctrl+D is an EOF for UNIX-based systems.) The eof( ) member function returns true if an end-of-file marker has been read.

good: A stream that is in none of the above states is in a good state, and the member function good( ) returns true for an object in this state. Note that good( ) simply reports the current state of an object and has nothing to say about whether the next operation will succeed.

Console Streams

These (at least the narrow versions using char) are declared in <iostream>. Strictly speaking, the Standard only requires that header to declare the eight standard console stream objects, and the declarations of the functionality are provided by the <istream> and <ostream> headers. In practice, most implementations include those headers nested in <iostream>.

File Streams

The <fstream> header provides the necessary declarations for the three subtypes ifstream (input only), ofstream (output only), and fstream (bidirectional). The file-stream subtypes provide all the functionality of the corresponding basic stream types (input stream, output stream, and bidirectional stream) plus facilities for opening and closing files. The destructor of a file-stream object always closes any associated file.

String Streams

The <sstream> header provides the necessary declarations for the three subtypes istringstream (input only), ostringstream (output only), and stringstream (bidirectional). Those types use a std::string as a source/sink for data. There are also a matching set of (i/o)wstringstream types that use a std::wstring as a source/sink for data. More advanced facilities are available for the expert specializing in I/O problems.

The special feature of the string-stream types is the provision of an overloaded str( ) member function. When called without arguments, it returns the buffer as a string (useful when using a stringstream to convert data to a std::string before using it elsewhere in a program). The version of str( ) that takes a std::string as an argument replaces the internal buffer with a copy of the argument.

char* Streams

The <strstream> header provides the necessary declarations for the three subtypes istrstream (input only), ostrstream (output only), and strstream (bidirectional). These use an array of char as a buffer. The Standard provides them simply as support for pre-standard code. They are officially deprecated, which means that the Standards Committee reserves the right to remove them from future releases of the C++ Standard.

272

CHAPTER 14

Manipulators

The C++ Standard provides a number of special objects that change the state of a stream. These are called manipulators. They are used by applying the appropriate insertion/extraction operator. For example,

int main( ){

std::cin >> std::hex; int i;

std::cin >> i;

std::cout << "\n\n" << i;

}

outputs ‘16’ if you type in ‘10’, i.e. it treats ‘10’ as a hexadecimal value. The commonest manipulators are:

std::endl: adds a newline character to output and flushes the output buffer. std::setw(n): sets the width of the field for the next output to n.

std::right and std::left: set the justification for the next output in the output field. std::dec, std::oct, and std::hex: set the base for subsequent use of the stream.

There are a number of others, as well as facilities for writing your own. For further details, refer to a good reference such as The C++ Standard Library.

C H A P T E R 15

Exceptions

We have been making use of exceptions from very early in this book but I have said very little about them. It is time that I remedied this, because exceptions have a fundamental impact on C++, way beyond their use for handling errors. The existence of exceptions changes the way we should write code. We cannot simply bolt exceptions on as an afterthought.

Though this is one of the shorter chapters, its contents are very important to your development as a competent C++ practitioner.

What Is an Exception?

Different programmers will give you different answers to this question. The differences between experts are very much a matter of emphasis. Here is my answer.

An exception is a situation that you can anticipate, where continuation of the normal code will fail, possibly catastrophically. For example, a program reading data from a file in which the data does not match the program’s requirements clearly cannot continue with anything that depends on the data. We do not expect corrupt files or files in the wrong format, but we do know that such things happen.

Most cases where an expectation has not been met require an alternative execution path. Sometimes that alternative may be to abandon the program, but at other times we may be able to recover and continue with the program’s work. Even if we have to abandon the program, we may still want to clean up first. The traditional styles of programming interleave error-handling code (i.e. code to deal with broken expectations) with normal code. This leads to fragile code that is often a nightmare to maintain and modify. Consider the following:

int main( ){

std::ifstream infile("data.txt"); if(not infile){

std::cerr << "Problem with opening data.txt.\n"; return EXIT_FAILURE; // declared in cstdlib

}

// process the file return EXIT_SUCCESS;

}

274

CHAPTER 15

The error-handling code is intrusive, but in this context we can live with it. However, what if that were not main( ) but some other function? Should we abandon program execution by calling abort( )? Should we report the error to the calling function? If the latter, how should we report the failure? The traditional solution is to provide some kind of error return code. However, that preempts at least one return value, and places a requirement on the calling function to check for the error return. What makes this worse is that often the calling function can only relay the failure report to the function that called it. Error return codes are fine when there is a reasonable expectation that the calling function will both check for an error and handle it locally. Once we go much beyond that, using an error return ceases to be a good solution.

Let me rewrite the above program using exceptions, and then consider how the new form works even when the function is not main( ):

int main( ){ try{

std::ifstream infile("data.txt");

if(not infile) throw "Problem with opening data.txt.\n"; // process the file

}

catch(char const * message){ std::cerr << message; return EXIT_FAILURE;

}

return EXIT_SUCCESS;

}

You may think that I should reorganize the above code to:

int main( ){

std::ifstream infile("data.txt"); if(infile){

// process the file return EXIT_SUCCESS;

}

else{

std::cerr << "Problem with opening data.txt.\n"; return EXIT_FAILURE;

}

}

Yes, I could, but that code relies on the code handling the problem locally. Now let me move the file use out of main( ):

void process(std::string const & filename){ std::ifstream infile(filename.c_str( ));

if(not infile) throw "Problem with opening data file in process( )."; // process the file

}

void process_file( ){ std::string filename;

std::cout << "Which file contains the data? "; std::cin >> filename;

process(filename);

}

EXCEPTIONS

275

int main( ){ try{

process_file( );

}

catch(char const * message){ std::cerr << message; return EXIT_FAILURE;

}

return EXIT_SUCCESS;

}

Do you see how throwing an exception disconnects detection of the problem (failure to open a file) from handling it (in this case, just reporting the problem and ending the program)? However, we gain even more, because there might be other problems incurred during the processing of a successfully opened file. Notice that the intermediate function, process file( ), has no need to provide any mechanism for reporting errors that may result from its call of process( ). The C++ exception mechanism provides this separation and thereby provides us with a way to write simpler code. With a little care, we can write code that retries when something fails. Here is a modified version of main( ):

int main( ){ try{

bool another(true); while(another){

try{ process_file( );

}

catch(char const * message){ std::cerr << message << '\n';

}

std::cout << "Do you want to process another file? "; std::cin >> another;

}

catch(...){

std::cerr << "Unknown exception caught.\n"; return EXIT_FAILURE;

}

return EXIT_SUCCESS;

}

The inner catch handles the ‘expected’ exception and allows the program to continue with trying another file. The outer catch will catch all other types of exceptions and terminate the program.

What Can I throw?

C++ places very few restrictions on what we can use as an exception object. As long as the object can be copied (and that is an absolute requirement, because the exception mechanism may need to move the exception object to a safe location while doing the stack cleanup), you can use it as an exception object. However, it is generally good practice to throw objects of types designed to provide exception objects. My use of a string literal in the example code above is poor coding practice. I used string literals because I did not want to get into the design of exception types until I had shown how exceptions simplify code.

276

CHAPTER 15

The C++ Library provides a hierarchy (based on class exception) of exception types for use within the Standard Library. We can use some of these in our own code (as I have in earlier chapters), but it is normally better to design exception types for our own use. Most of these can be very simple. Indeed, we can often use stateless classes (ones with no data members) as exception types. We can use the Standard Library types as bases for our own types when that seems suitable.

We normally nest exception types in the class that will use them. For example, our chessboard type has a number of ways in which it can fail. This is particularly true of the constructor that tries to create a chessboard object from data provided by a stream. Unlike other functions, constructors can only reliably report failure by throwing an exception. By using the exception mechanism to deal with failed construction (for example, because it has been impossible to place the object into a destructible state), we can assume that defined objects in our code exist in a stable state that meets the class invariants (those properties that class objects are required to have).

class chessboard{ public:

chessboard( ); chessboard(std::istream &); ~chessboard( );

struct exception{ };

struct bad_data: exception{ }; struct invalid_piece: bad_data{ }; struct invalid_position: bad_data{ }; struct corrupt_stream: exception{ };

//rest elided private:

//details elided

};

I have added an entire exception hierarchy into chessboard. Each of the exception types in that hierarchy is a stateless class. It does not have to be; for example, I could add a (virtual) member function that reports the type of the exception:

class chessboard{ public:

chessboard( ); chessboard(std::istream &); ~chessboard( );

struct exception{

virtual char const * report( )const{ return "Generic chessboard exception.";

}

};

struct bad_data: exception{

virtual char const * report( )const{ return "Bad data chessboard exception.";

}

};

struct invalid_piece: bad_data{ virtual char const * report( )const{

return "Invalid piece chessboard exception.";

}

EXCEPTIONS

277

};

struct invalid_position: bad_data{ virtual char const * report( )const{

return "Invalid position chessboard exception.";

}

};

struct corrupt_stream: exception{ virtual char const * report( )const{

return "Corrupt stream chessboard exception.";

}

};

//rest elided private:

//details elided

};

Now you can write:

int main( ){ try{

std::ifstream data("Chessposition.txt"); if(not data) throw "No such file."; chessboard board(data);

}

catch(char const * message){ std::cerr << message << '\n'; return EXIT_FAILURE;

}

catch(chessboard::exception const & error){ std::cerr << error.report( ) << '\n'; return EXIT_FAILURE;

}

catch(...){

std::cerr << "Unknown exception caught.\n"; return EXIT_FAILURE;

}

return EXIT_SUCCESS;

}

Note that the above code catches the possible chessboard exceptions with a const reference to chessboard::exception. In general, we should catch exceptions by reference, so that we preserve any polymorphic behavior provided by possible subtypes. It is also common to catch by const reference, because we would not normally want to change the data encapsulated in an exception

object.

 

 

 

 

 

 

 

 

 

When there is a list

of catch

clauses, the

program

executes the

first

one

that

matches

the

actual exception (even

if that involves a type conversion). Note

that

this

is

different

from

function overloading,

where the

compiler

attempts

to determine

a unique

best

match.

For example, adding a catch(chessboard::corrupt stream const & cs) handler after the catch(chessboard::exception const & error) handler will do nothing, because the latter will grab the exception and process it. More specialized exceptions (i.e. ones that are derived from a base) must be placed earlier in the list of catch clauses.