I am making chess, it's a great way to practice basic OOP. There's an abstract base class Piece
with subclasses like King
, Rook
, Pawn
, etc. as well as a Board
class.
Is there a way to create multiple overloaded functions in the board class, which perform different operations depending on the parameter's subclass? Here's what I mean, in very barebones class structure:
class Piece {};
class King : public Piece {};
class Rook: public Piece {};
class Pawn: public Piece {};
class Board {
public:
void move_piece(King piece) {
;
}
void move_piece(Rook piece) {
;
}
void move_piece(Pawn piece) {
;
}
};
int main() {
Board game;
Pawn pawn;
Piece p = pawn;
game.move_piece(p);
}
I've tried implementing this, however when I pass a piece to this overloaded function, it's declared as a Piece
, not a Rook
or Pawn
. This leads to the following error:
no suitable user-defined conversion from "Piece" to "Pawn" exists
Fair enough. But is it possible to accomplish what I'm trying to do?
Let's start with some mock implementations and see where this gets us:
void move_piece(Pawn piece) {
//get user to move the piece
Move mv = GetUserToChoose(possiblePawnMoves(piece));
if(mv.isNull()) { //if user decided he doesn't want to move this piece
//??? How do we signal he should choose another?
return;
}
piece.move(mv);
}
Oops. piece
within this function would be a copy of pawn
outside so any changes to its own state will be discarded when you exit move_piece()
. Do you want to change some internal pawn
state? For example, consider whether the pawn is legible for en passant: who, exactly, has to memorize that? If it's the pawn
then
Piece p = pawn;
is a terrible idea. Well, it's a terrible idea in general: when you assign an object of derived type to the object of its base type, the parts unique to the derived object don't get copied! It's called slicing and is almost never what you wanted to do. (In addition, you're making another copy: any changes of p
won't affect pawn
, even within their common state.) If your different objects have their own states, not saved in a general Piece
, then you should keep them in variables of their proper types.
So you could do something like:
void move_piece(Pawn& piece){ //we potentially change pawn state
Move mv = GetUserToChoose(possiblePawnMoves(piece));
if(mv.isNull()) { //if user decided he doesn't want to move this piece
//??? How do we signal he should choose another?
return;
}
piece.move(mv);
}
//...
Board game;
Pawn pawn;
game.move_piece(p); //compiler chooses overload Board::move_piece(Pawn&)
All is good? Eh... why did you try to pass a Piece
object in the first place? Probably because you want to have a set of Piece
s and move them in a uniform fashion, something like
std::vector<Piece> pieces;
//...
board.move_piece(pieces[i]); // want to call Board::move_piece(Rook&)
++i;
board.move_piece(pieces[i]); // want to call Board::move_piece(Knight&)
As written, it can't be done. Sorry. The compiler has to decide what function address to put in place of the call, but Board::move_piece(Rook&)
and Board::move_piece(Knight&)
are different functions with different addresses. In other words, you need to make the call address dependent on runtime data somehow. One option (described in Jan Schultke answer) would be to invert the choice logic and use virtual functions:
class Pawn : public Piece {
//...
virtual void move_on(Board& board){
board.move_piece(*this); //overload resolution chooses Board::move_piece(Pawn&)
}
//...
};
//...
std::vector<std::unique_ptr<Piece>> pieces; //save POINTERS, no slicing
//...
pieces[i]->move_on(board); // invokes Board::move_piece(Rook&);
++i;
pieces[i]->move_on(board); // invokes Board::move_piece(Knight&);
(technically this is achieved via (pieces[i]->pVTable)[MOVE_FUNC](board)
where pVTable
is a "hidden" member of Piece
filled with different addresses in constructors of different classes). Alternatively you may use a simpler approach and dispatch the call yourself:
enum class PieceType {
Pawn,
Rook,
//...
};
class Piece {
public: PieceType type;
Piece(PieceType t) : type(t) {}
//...
};
class Pawn : public Piece {
//...
Pawn(void) : Piece(PieceType::Pawn) {}
//...
};
//...
class Board {
void move_piece_dispatch(Piece* piece){
if(piece == nullptr) return MayStartPanickingNow();
switch(piece->type){
case PieceType::Pawn: return move_piece(static_cast<Pawn&>(*piece));
//...
default: return MayStartPanickingNow();
}
}
//...
std::vector<std::unique_ptr<Piece>> pieces;
//...
board->move_piece_dispatch(pieces[i].get()); // invokes Board::move_piece(Rook&);
++i;
board->move_piece_dispatch(pieces[i].get()); // invokes Board::move_piece(Knight&);
Alternatively alternatively, if you want to get some interesting practice, you may keep your own table of Board
functions in Piece
subtypes:
class Piece {
//...
std::function<void(Board&)> move;
//...
};
class Pawn : public Piece {
Pawn() {
move = [this](Board& board){return board.move_piece(*this);};
}
//...
};
//...
std::vector<std::unique_ptr<Piece>> pieces;
//...
pieces[i]->move(board); // invokes Board::move_piece(Rook&);
++i;
pieces[i]->move(board); // invokes Board::move_piece(Knight&);
The function overload is chosen based on object type in its constructor and saved in the std::function
object for future use.