1: The king/rook involved must not have been moved before. If it has made a move already it can't castle.

2: There must be no pieces between it and the rook.

3: You can not castle when you are in check.

So with them in mind are you asking how you would know when the king can castle without knowing the other pieces information on the board?

Not sure I am understanding the question exactly, but you could have conditions in place to first check if the king has been moved, then if there is any pieces between it and the rook it wants to castle with, and then lastly if it is in check.

TBH. I'd just have an enum that said what type each piece was and assign that to the piece in the constructor. It saves any RTTI or casting on the fly. It could also be useful when trying to determine if you're in check/checkmate, when pawn hits the end of the board etc.

Edit: yes it's not the "perfect" solution. But it's simple to implement, simple to understand and would be simple for another developer to maintain (I'd say the most important aspect of any piece of code)

@Lachlan Easton: The problem is that I can't see how not to avoid the type check or cast. Even with cire's strategy pattern and files that describe moves, the file would have to say "this move only works with this specific kind of piece". That feels like cheating.

I've already implemented en passant without such cheating, I'm 100% confident that there is a way to solve this problem too - I just don't know what it is, which is why I started this thread.

Create a virtual function bool ChessPiece::canCastle(const ChessPiece& other) and protected virtual one ECastlingType ChessPiece::castlingType(), which returns CT_NOT_ELIGIBLE if it can not castle and CT_MAIN or CT_SUPPORT if it is king or rook respectively.
canCastle function will use castlingType and some private data (bool hasMoved) to decide if you can castle.

You may not even need to be that specific. For each piece you could keep track of whether or not its moved before, if a piece in any corner hasn't moved then it is guaranteed to be a rook (unless there's some custom board layout shudder) and so without checking its type or canCastle you can know that the king can castle with it.

Well, it was an generalisation suppurting different board layouts, castling strategies and anything else.
Your solution is enough for this actually. We know assumed positions of both king and rooks, so we can just check if they moves.

EDIT: Also hasMoved function is useful to determine if pawns can move two squares too (or just check if they are on their starting position).