Search code examples
c++inheritanceoverloadingsubclass

C++: overloading method with parameters of multiple subclasses


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?


Solution

  • 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 Pieces 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.