You Can Program In C++ (2006) [eng]
.pdf
248 |
CHAPTER 13 |
Strictly speaking, I had no need to make move( ) a pure virtual function because there is perfectly reasonable behavior for the abstract piece – behavior that every actual piece will have. I made move( ) a pure virtual so that I could demonstrate the provision of an implementation for such a function and show how it is used by the subtypes. I will say more about that when we deal with the subtype implementations of move( ).
I have used what( ) as an example of the commoner form of pure virtual, which is not implemented in the base class. I have also changed what( ) to match the form I am using in chesspiece, one that returns the name of the piece in a string.
The rest of basic chesspiece is much the same as the earlier version. Here is its implementation, which can be added immediately after the definition:
basic_chesspiece::basic_chesspiece(bool white, bool castle) :white_(white), location_(off_board), can_castle_(castle){
if(instrument)
std::clog << "basic_chesspiece constructor 1 (off-board) called.\n";
}
basic_chesspiece::basic_chesspiece(position const & location, bool white, bool castle)
:white_(white), location_(location.is_valid( )), can_castle_(castle){ if(instrument)
std::clog << "basic_chesspiece constructor 2 (with location) called.\n";
}
basic_chesspiece::~basic_chesspiece( ){
if(instrument) std::clog << "basic chesspiece destructor called.\n";
}
bool basic_chesspiece::move(position const & loc){ location_ = loc.is_valid( );
return true;
}
I have modified the instrumentation so that it sends messages to std::clog rather than std::cout. That is the kind of thing std::clog was designed for.
Defining and Implementing the Subtypes
The first thing to note is that nothing needs access to the individual subtypes (pawn, bishop, etc.) other than the implementation of chesspiece. That is a broad design hint that we should tuck them away in the unnamed namespace for chess2.cpp. I am not going to waste space giving you the definitions and implementations of all the subtypes. But here are three examples:
namespace{
class knight: public basic_chesspiece{ public:
virtual std::string what( )const; virtual bool move(position const &); explicit knight(bool white = true);
explicit knight(position const &, bool white = true); virtual ~knight( );
private:
DYNAMIC OBJECT CREATION AND POLYMORPHIC OBJECTS |
249 |
};
class pawn: public basic_chesspiece{ public:
virtual std::string what( )const; virtual bool move(position const &); explicit pawn(bool white = true);
explicit pawn(position const &, bool white = true); virtual ~pawn( );
private:
};
class king: public basic_chesspiece{ public:
virtual std::string what( )const; virtual bool move(position const &); explicit king(bool white = true);
explicit king(position const &, bool can_castle = true, bool white = true);
virtual ~king( ); private:
bool castle(position const & destination);
};
// other effectively identical definitions omitted
}
Notice that the second constructor for a king is different from the other two. The subtypes provide their own constructors and destructor, and the two virtual functions. We saw earlier that the king needs an extra helper function to deal with its castling move. You may also have provided extra functionality to cater for the special moves available to a pawn.
Here is an implementation for those subtypes. I have relied largely on stub functions because my emphasis is on the C++ technology, not the fine detail of writing code to provide correct behavior for chess pieces.
namespace{
// implementation of knight subtype
knight::knight(bool white):basic_chesspiece(white){ if(instrument) std::clog << "knight constructor 1 called.\n";
}
knight::knight(position const & pos, bool white) :basic_chesspiece(pos, white){
if(instrument) std::clog << "knight constructor 2 called.\n";
}
knight::~knight( ){
if(instrument) std::clog << "knight destructor called.\n";
}
bool knight::move(position const & destination){ destination.is_valid( );
position const current(where( ));
int const rank_dif(std::abs(current.rank - destination.rank)); if(rank_dif > 2 or rank_dif < 1) return false;
250 |
CHAPTER 13 |
int const file_dif(std::abs(current.file - destination.file)); if(file_dif > 2 or file_dif < 1) return false;
if(rank_dif + file_dif != 3) return false; return basic_chesspiece::move(destination);
}
std::string knight::what( )const{ return "knight";
}
// implementation of pawn subtype
pawn::pawn(bool white):basic_chesspiece(white, true){ if(instrument) std::clog << "pawn constructor 1 called.\n";
}
pawn::pawn(position const & pos, bool white) :basic_chesspiece(pos, white, true){
if(instrument) std::clog << "pawn constructor 2 called.\n";
}
pawn::~pawn( ){
if(instrument) std::clog << "pawn destructor called.\n";
}
bool pawn::move(position const & destination){ std::cout << "Not implemented.\n";
return true;
}
std::string pawn::what( )const{ return "pawn";
}
// implementation of king subtype
king::king(bool white):basic_chesspiece(white, true){ if(instrument) std::clog << "king constructor 1 called.\n";
}
king::king(position const & pos, bool cc, bool white) :basic_chesspiece(pos, white, cc){
if(instrument) std::clog << "king constructor 2 called.\n";
}
king::~king( ){
if(instrument) std::clog << "king destructor called.\n";
}
bool king::move(position const & destination){ destination.is_valid( );
position const current(where( ));
int const rank_dif(std::abs(current.rank - destination.rank)); if(rank_dif > 1) return false;
int const file_dif(std::abs(current.file - destination.file)); if(rank_dif == 0 and file_dif == 2){
DYNAMIC OBJECT CREATION AND POLYMORPHIC OBJECTS |
251 |
return castle(destination); // delegate to special function
}
if(file_dif > 1) return false; can_castle(false); // lose ability to castle return basic_chesspiece::move(destination);
}
std::string king::what( )const{ return "king";
}
bool king::castle(position const & destination){ std::cout << "Castling has not been implemented.\n"; return false;
}
// plus similar code for the other chess pieces
}
Constructing a Specific Chess Piece
The following is a helper function (based on the one we wrote in the last chapter) to construct the right chess piece on demand:
namespace{
// helper function for chesspiece constructor and for transform std::auto_ptr<basic_chesspiece> make(chesspiece::piece p,
chesspiece::position pos1, bool white, bool can_castle = false){ position pos(pos1.file, pos1.rank);
std::auto_ptr<basic_chesspiece> piece_ptr(0); switch(p){
case chesspiece::knight:
piece_ptr = std::auto_ptr<basic_chesspiece>(new knight(pos, white)); break;
case chesspiece::bishop:
piece_ptr = std::auto_ptr<basic_chesspiece>(new bishop(pos, white)); break;
case chesspiece::rook: piece_ptr =
std::auto_ptr<basic_chesspiece>(new rook(pos, can_castle, white)); break;
case chesspiece::queen:
piece_ptr = std::auto_ptr<basic_chesspiece>(new queen(pos, white)); break;
case chesspiece::king: piece_ptr =
std::auto_ptr<basic_chesspiece>(new king(pos, can_castle, white)); break;
case chesspiece::pawn:
piece_ptr = std::auto_ptr<basic_chesspiece>(new pawn(pos, white)); break;
252 CHAPTER 13
default: piece_ptr =
std::auto_ptr<basic_chesspiece>(new indeterminate(pos, white));
}
return piece_ptr;
}
}
Note how the std::auto ptr<> instances relay the ownership of the dynamic instance back to the caller by returning by value. The local piece ptr goes out of scope and is destroyed, but it has already passed responsibility for the lifetime of the freshly created piece to the return value.
The chesspiece Constructor and transform( )
The chesspiece constructor knows which piece it needs, and calls make( ) to get the one it needs before storing the address of the new piece safely in its piece ptr data member. This magic of looking after the
lifetime of a dynamic object, even one of a polymorphic type, is precisely the job for which std::auto ptr was designed. Here is the implementation of chesspiece’s constructor:
chesspiece::chesspiece(piece p, position pos, bool white, bool castle) :piece_ptr(make(p, pos, white)){
if(instrument) std::clog << "Chesspiece constructed.\n";
}
Simple when you know how. One point to notice is that helper functions like make( ) are particularly useful for ensuring that we do all we can within the constructor initializer list. But it has a second use; here is the implementation of transform( ):
void chesspiece::transform(piece p){
piece_ptr = make(p, where( ), is_white( ), piece_ptr -> can_castle( ));
}
This function relies on the special characteristic of the assignment operator for std::auto ptr<>: when you assign a std::auto ptr<> to another one, the left-hand one first ends the lifetime of the object it is currently responsible for before taking on the responsibility for the one it is acquiring. Of, course the right-hand std::auto ptr<> has to release its responsibility. Now you know why std::auto ptr<> has a non-standard copy-assignment operator: it needs it to do the job it is designed to do.
Implementing the Rest of chesspiece
There is not much more to do, because chesspiece just delegates the work to basic chesspiece, which in turn delegates much of the work to the implementations of the subtypes (the individual types of chess piece).
First, the non-virtual member functions:
position chesspiece::where( )const{return piece_ptr -> where( );} bool chesspiece::is_white( )const{return piece_ptr -> is_white( );}
bool chesspiece::can_castle( )const{return piece_ptr -> can_castle( );} void chesspiece::can_castle(bool cc){piece_ptr -> can_castle(cc);}
DYNAMIC OBJECT CREATION AND POLYMORPHIC OBJECTS |
253 |
Next, the member functions that forward to virtual versions:
std::string chesspiece::what( )const{return piece_ptr -> what( );}
bool chesspiece::move(position const & p){return piece_ptr -> move(p);}
Finally, we have the destructor:
chesspiece::~chesspiece( ){
if(instrument) std::clog << "Chesspiece destroyed.\n";
}
Before we forget, there is still a member function from chesspiece::position to deal with:
position const & chesspiece::position::is_valid( )const{
if((file == off_board.file) and (rank == off_board.rank)) return *this; if(rank > 7) throw std::out_of_range("Invalid rank");
if(file > 7) throw std::out_of_range("Invalid file"); return *this;
}
T R Y T H I S
When you have added the above code to chess2.cpp, you will find that our program from ‘A Chess-Piece Type’ (page 244) will build and run. Try it with the instrumentation on so that you can see all the work that goes on under the hood. When you have done that, add code to the program to test the rest of the public interface of chesspiece.
T R Y T H I S
Experiment 5
Switch the instrumentation on (by setting the value of instrument in chess2.cpp to true), and build and execute this short program:
int main( ){ chesspiece cp;
}
Now go to chess2.cpp and remove the virtual qualifier from the declaration of the destructor for basic chesspiece. Build and execute the program again. Notice that the compiler issues a warning about a class with virtual functions and a non-virtual destructor (well, it does with the compiler shipped with this book when the warning level is set high enough); while C++ allows this, it is usually a design error.
Study the output and you should notice that the destructor for indeterminate has not been called. That failure is harmless for this particular polymorphic hierarchy because the subtype destructors do not actually do anything. However, the omission is generally dangerous (C++ categorizes calling a base destructor without first calling the derived destructor as undefined behavior – anything may happen). Just as for any other member function accessed through a pointer or reference, non-virtual destructors will be those for the type of the pointer or reference rather than for the real type of the instance referenced. Please do not forget this; it is an error to destroy an instance without calling the destructor for the exact type.
254 |
CHAPTER 13 |
Experiment 6
Modify the above program to:
int main( ){ chesspiece cp[2];
std::sort(cp, cp + 2);
}
Now try to compile it. You will get a veritable cascade of errors (143 when I tried it). If you comment out the attempt to sort the array, the errors disappear, from which we can deduce that the problem is not with creating the array but with attempting to sort it. Quite right too – chesspiece is an entity type with copy semantics suppressed. We can confirm that by temporarily commenting out the private declarations of the copy constructor and copyassignment operator in the definition of chesspiece. Try it. However, do not try to execute the resulting program, because the compiler-generated copy constructor interacts badly with std::auto ptr<>. Remember that copying a std::auto ptr<> transfers ownership. If we want ‘safe’ copy semantics for a type like chesspiece, we must decide what we mean by copying one. Either we must share ownership of the underlying object, or we must be willing to clone it (i.e. create a new, distinct object with identical value). We get the former by using a different smart pointer such as Boost’s shared ptr<> (see http://www.boost.org/). If we want the latter, we have to write our own copy functions to provide ‘deep copying’. That term refers to copying that includes duplicating the underlying objects that are owned through some kind of pointer.
Experiment 7
Modify the above program to:
int main( ){ chesspiece * cp[2];
cp[0] = new chesspiece(chesspiece::rook); cp[1] = new chesspiece(chesspiece::pawn); std::sort(cp, cp + 2);
}
Ignore, for now, that we have not destroyed the dynamic instances of chesspiece; instead, focus on what std::sort is doing. It is just sorting the addresses of those dynamic instances. That is not likely to be what we intended. We have to provide a function that will provide some ordering for the instances rather than for their addresses. Here is one (which sorts chesspieces alphabetically):
bool chesspiece_order(chesspiece const * const lhs, chesspiece const * const rhs){
return lhs->what( ) < rhs->what( );
}
Now expand the test program to:
int main( ){ chesspiece * cp[2];
cp[0] = new chesspiece(chesspiece::rook); cp[1] = new chesspiece(chesspiece::pawn);
DYNAMIC OBJECT CREATION AND POLYMORPHIC OBJECTS |
255 |
std::sort(cp, cp + 2, chesspiece_order); delete cp[0];
delete cp[1];
}
Note that it uses the alternative form of std::sort( ), where you provide a predicate (a function returning a bool) to define the order. Build and execute that program, and note the order in which the destructors are called (you will need the instrumentation on for this). Now repeat the exercise with the sort commented out, so that you can check that the sort changes the order.
EXERCISES
1.Write a replacement for the chesspiece order( ) function, to order the pieces according to their position. For example, order by rank and then by file.
2.Write a program that displays a collection of chesspieces on a chessboard. Note that your solution to Exercise 1 will help with this task. Identify the different chess pieces by a suitable letter, uppercase for white and lowercase for black.
S T R E T C H I N G E X E R C I S E S
3.Write a complete hierarchy for checkers (draughts) pieces. This is easier than for chess because there are only two types of piece (plain and kings), but it still has the characteristic that plain pieces can be promoted to kings.
4.Add a board type that tracks where the pieces are. Note that you will need to provide a facility for querying a square to find out whether it is occupied and, if so, by what color of piece.
5.Extend your solution to Exercise 3 so that a piece can determine whether it can make a capture move. It will need to query the board object. If you work systematically, this exercise is not as difficult as it might seem.
Collections of Objects
Experiment 3 above demonstrates one of the major problems with collections of entities. If they cannot be copied, we have a problem with using the Standard Library algorithms for them, because those algorithms largely expect that they can copy the objects they are working with. We have two
main choices: we can use containers of a suitable type of smart |
pointer (one that supports stan- |
||
dard copy semantics, such as Boost’s shared ptr<>); or we can |
encapsulate the collection into a |
||
|
|
|
|
class that will look after the lifetime of the entities it holds. The design will largely depend on our intentions.
In this section, I am going to show how we can implement the concept of a collection of chess pieces on a chessboard.
256 CHAPTER 13
Design and Implementation of a chessboard Type
Here is a suitable class definition:
class chessboard{ public:
chessboard( ); ~chessboard( );
void remove_piece(chesspiece::position);
void chessboard::insert_piece(chesspiece::piece, chesspiece::position, bool white = true, bool can_castle = false)
void move_piece(chesspiece::position destination, chesspiece::position source);
chesspiece const * contains_piece(chesspiece::position)const; private:
chesspiece * board[64]; // disable copying
chessboard(chesspiece const &);
chessboard & operator=(chessboard const &);
};
There is very little to the basic design of a chessboard type. We need to be able to construct and destroy one. We need to be able to add pieces to the board and remove them from the board. We need to be able to move a piece. Finally, we need to be able to ask what is on a specific square. Notice that contains piece( ) returns a chesspiece const *. You might be tempted to return a chesspiece const & instead, but that does not work, because it will not allow us to handle empty squares.
Here is an implementation:
chessboard::chessboard( ):board( ){
if(instrument) std::clog << "Chess board constructed.\n";
}
chessboard::~chessboard( ){
if(instrument) std::clog << "Destroying pieces.\n"; for(int i(0); i != 64; ++i){
if(instrument and board[i]) std::clog << board[i]->what( )
<< " at " << i % 8 << ", " << i / 8 << ".\n"; delete board[i];
}
if(instrument) std::clog << "Chess board emptied and destroyed.\n";
}
void chessboard::remove_piece(chesspiece::position p){ delete board[p.rank * 8 + p.file];
board[p.rank * 8 + p.file] = 0;
}
void chessboard::insert_piece(chesspiece::piece pc, chesspiece::position p, bool white, bool can_castle){
board[p.rank * 8 + p.file] = new chesspiece(pc, p, white, can_castle);
}
void chessboard::move_piece(chesspiece::position destination,
DYNAMIC OBJECT CREATION AND POLYMORPHIC OBJECTS |
257 |
chesspiece::position source){ board[destination.rank * 8 + destination.file]
= board[source.rank * 8 + source.file]; board[source.rank * 8 + source.file] = 0;
}
chesspiece const * chessboard::contains_piece(chesspiece::position p)const{ return board[p.rank * 8 + p.file];
}
As you see, most of the implementation is straightforward. The destructor is the only slightly complicated function. Even there, the complexity is more apparent than real, because most of the code is instrumentation.
T R Y T H I S
Add the definition of chessboard to chess2.h, and then add the implementation to chess2.cpp. When chess2.cpp compiles (i.e. you have dealt with any typos), use the following for testing:
int main( ){ chessboard b;
b.insert_piece(chesspiece::pawn, chesspiece::position(2, 3)); b.insert_piece(chesspiece::king, chesspiece::position(4, 3), false); b.insert_piece(chesspiece::rook, chesspiece::position(0, 0),
true, true);
b.move_piece(chesspiece::position(3, 3), chesspiece::position(2, 3)); chesspiece const * p(0);
p = b.contains_piece(chesspiece::position(4, 3));
if(p) std::cout << "That square contains a " << p->what( ) << ".\n"; else std::cout << "That square is empty.\n"; b.remove_piece(chesspiece::position(4, 3));
p = b.contains_piece(chesspiece::position(4, 3));
if(p) std::cout << "That square contains a " << p->what( ) << ".\n"; else std::cout << "That square is empty.\n";
}
S T R E T C H I N G E X E R C I S E S
6.Add a display function to chessboard that shows the current board, using suitable upperand lowercase letters for the black and white pieces.
7.My implementation for chessboard assumes that the user always supplies legal values for chesspiece::position data. Add suitable validation code to trap cases where the provided values do not resolve to a square on the board. As you will need this code several times, it is probably best provided as a helper function in the unnamed namespace for chess2.cpp.
8.Write a set of functions that iterates over all the squares of the board and computes how many white pieces currently attack (i.e. can move directly to) each one. Initially, you can ignore pieces blocking the moves of other pieces. However, a complete solution will take that into account. Your code should be able to display the result as an eight-by-eight grid of integer values.
