I'm currently working on a performance-oriented chess move generator and using lots of strongly-typed enum class declarations for things like Square, File, Rank, Color, PieceType, Castling, Direction etc. I faced an issue that I constantly need to convert enum class values to their underlying type (uint8_t) and back to use them as array indices, perform arithmetic, pass as arguments, or combine them in logic. This leads to tons of repetitive and ugly static_cast<uint8_t>(...) all over the code, as in the following MRE:
#include <array>
#include <cstdint>
#include <cstdlib>
#include <iostream>
using u8 = uint8_t;
using Bitboard = uint64_t;
enum class File : u8 {
FA, FB, FC, FD, FE, FF, FG, FH, NB
};
enum class Rank : u8 {
R1, R2, R3, R4, R5, R6, R7, R8, NB
};
enum class Square : u8 {
A1, B1, C1, D1, E1, F1, G1, H1,
A2, B2, C2, D2, E2, F2, G2, H2,
A3, B3, C3, D3, E3, F3, G3, H3,
A4, B4, C4, D4, E4, F4, G4, H4,
A5, B5, C5, D5, E5, F5, G5, H5,
A6, B6, C6, D6, E6, F6, G6, H6,
A7, B7, C7, D7, E7, F7, G7, H7,
A8, B8, C8, D8, E8, F8, G8, H8,
NB, FIRST = A1, LAST = H8
};
enum class Color : u8 { WHITE, BLACK, NB };
enum class PieceType : u8 { PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING, NB };
constexpr File getFile(Square sq) {
return static_cast<File>(static_cast<u8>(sq) % 8);
}
constexpr Rank getRank(Square sq) {
return static_cast<Rank>(static_cast<u8>(sq) / 8);
}
std::array<std::array<u8, static_cast<u8>(Square::NB)>, static_cast<u8>(Square::NB)> squareDistance{};
std::array<std::array<Bitboard, static_cast<u8>(Square::NB)>, static_cast<u8>(Color::NB)> pawnAttacks{};
std::array<std::array<Bitboard, static_cast<u8>(Square::NB)>, static_cast<u8>(PieceType::NB)> pseudoAttacks{};
void precomputeSquareDistance() {
for (u8 s1 = static_cast<u8>(Square::FIRST); s1 <= static_cast<u8>(Square::LAST); ++s1) {
for (u8 s2 = static_cast<u8>(Square::FIRST); s2 <= static_cast<u8>(Square::LAST); ++s2) {
squareDistance[s1][s2] =
std::abs(static_cast<int>(getFile(static_cast<Square>(s1))) - static_cast<int>(getFile(static_cast<Square>(s2)))) +
std::abs(static_cast<int>(getRank(static_cast<Square>(s1))) - static_cast<int>(getRank(static_cast<Square>(s2))));
}
}
}
int main() {
precomputeSquareDistance();
std::cout << static_cast<int>(squareDistance[static_cast<u8>(Square::A1)][static_cast<u8>(Square::H8)]) << "\n";
return 0;
}
I chose enum class on purpose because I have too many enums in my full codebase. Also I prefer unsigned char over int as it boosts performance.
However, my decisions come at the cost of verbosity and overly cluttered code.
I tried solving the problem with a macro to enable "u8-compatible" operators (e.g. overloads for +, -, ++, comparisons, etc.) for general enum class types. That way I could avoid writing all these casts. But that quickly turned into a millon possible combinations of enum types and uint8_t, plus the need for templates or macro metaprogramming, and it just felt wrong and unmaintainable. Also it did not solve the problem of accessing the array directly.
Is there a clean, safe, and non-insane way to work with enum class values as integers without spamming static_cast?
What do experienced C++ developers do in such cases? Am I missing some simple design pattern?
Thanks in advance.
But that quickly turned into a millon possible combinations of enum types and uint8_t
Your code does not demonstrate this need. It might exist in your real code, but at the same time, you might be trying to over-engineer and tackle too many problems at once. Instead of treating this as a monolithic problem, divide and conquer. I see four distinct problems that can be handled individually.
NB
values are very likely to be used as numbers rather than as enumerators. So define constants for them with the desired integer type. This allows you to skip the cast where these numbers are used.constexpr u8 SquareCount = static_cast<u8>(Square::NB);
constexpr u8 ColorCount = static_cast<u8>(Color::NB);
constexpr u8 PieceCount = static_cast<u8>(PieceType::NB);
Square
. That is easy to add, and as a unary operator, there is intrinsically no need to account for combinations with other types.Square& operator++(Square& src) noexcept {
src = static_cast<Square>(1 + static_cast<u8>(src));
return src;
}
getFile
and getRank
functions are good, they are the cause of some of your problems. They should be supplemented by parallel functions that omit the final cast; i.e. that return u8
. I would use "Number" as a suffix to indicate that the return value is a number, suitable for use in calculations. You might want to modify getFile
and getRank
to call these new functions to avoid repeating the arithmetic.constexpr u8 getFileNumber(Square sq) {
return static_cast<u8>(sq) % 8;
}
constexpr u8 getRankNumber(Square sq) {
return static_cast<u8>(sq) / 8;
}
operator[]
cannot be defined outside a class. So my solution would be to define a class to wrap your array. You might find this has other benefits as well, such as being able to move precomputeSquareDistance
to the constructor. And not just a constructor, but a constexpr
constructor (the values might be computed during compilation instead of at runtime).class Distances {
std::array<std::array<u8, SquareCount>, SquareCount> squares;
public:
// Note: C++23 feature used here. For earlier versions, this takes
// a bit more boilerplate.
constexpr u8& operator[](Square s1, Square s2) {
return squares[static_cast<u8>(s1)][static_cast<u8>(s2)];
}
// There should also be a `const` version. That complication is routine
// and left to the reader.
constexpr Distances() {
for (Square s1 = Square::FIRST; s1 <= Square::LAST; ++s1) {
for (Square s2 = Square::FIRST; s2 <= Square::LAST; ++s2) {
// Note the comma between the subscripts rather than using
// two pairs of square brackets.
(*this)[s1, s2] =
std::abs(getFileNumber(s1) - getFileNumber(s2)) +
std::abs(getRankNumber(s1) - getRankNumber(s2));
}
}
}
};
Taken together, these changes reduce the occurrences of static_cast
in your demo code from 25 to 10. The key is that, aside from one cast in the main
function, the casts have been moved to utility functions and constants. The utilities are what get used repeatedly, not static_cast
directly. There might be more utilities needed in your real code, but it might not be as bad as you thought. Especially if you stop making things more difficult for yourself (see #3).
I acknowledge the answer about using unscoped enums within a namespace as another reasonable approach. Certainly worth considering if you are not able to move enough of the casts in your real code to utility functions.
For reference, the main function after introducing the above:
int main() {
std::cout << static_cast<int>(squareDistance[Square::A1, Square::H8]) << "\n";
return 0;
}
The static_cast
that remains in this function is an artifact of using uint8_t
and is not related to using scoped enumerations.