Atualmente, estou trabalhando em um gerador de jogadas de xadrez voltado para desempenho e usando muitas declarações de classes enum fortemente tipadas para funções como Quadrado, Arquivo, Classificação, Cor, Tipo de Peça, Roque, Direção, etc. Enfrentei o problema de precisar converter constantemente valores de classes enum para o tipo subjacente (uint8_t) e vice-versa para usá-los como índices de array, realizar operações aritméticas, passá-los como argumentos ou combiná-los em lógica. Isso resulta em toneladas de static_cast<uint8_t>(...) repetitivos e feios por todo o código, como no seguinte 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;
}
Escolhi a classe enum de propósito porque tenho muitas enumerações em todo o meu código-fonte. Também prefiro unsigned char em vez de int, pois melhora o desempenho.
No entanto, minhas decisões vêm ao custo de verbosidade e código excessivamente confuso.
Tentei resolver o problema com uma macro para habilitar operadores "compatíveis com u8" (por exemplo, sobrecargas para +, -, ++, comparações, etc.) para tipos de classes enum gerais. Dessa forma, eu poderia evitar escrever todas essas conversões. Mas isso rapidamente se transformou em um milhão de combinações possíveis de tipos enum e uint8_t, além da necessidade de modelos ou metaprogramação de macros, e simplesmente parecia errado e insustentável. Além disso, não resolveu o problema de acessar o array diretamente.
Existe uma maneira limpa, segura e não insana de trabalhar com valores de classe enum como inteiros sem usar static_cast?
O que desenvolvedores experientes em C++ fazem nesses casos? Estou esquecendo de algum padrão de design simples?
Desde já, obrigado.
Seu código não demonstra essa necessidade. Ela pode existir no seu código real, mas, ao mesmo tempo, você pode estar tentando criar problemas demais e lidar com muitos problemas de uma só vez. Em vez de tratar isso como um problema monolítico, divida para conquistar. Vejo quatro problemas distintos que podem ser resolvidos individualmente.
NB
valores sejam usados como números em vez de enumeradores. Portanto, defina constantes para eles com o tipo inteiro desejado. Isso permite que você pule a conversão onde esses números são usados.Square
. Isso é fácil de adicionar e, como um operador unário, não há necessidade intrínseca de levar em conta combinações com outros tipos.getFile
egetRank
sejam boas, elas são a causa de alguns dos seus problemas. Elas devem ser complementadas por funções paralelas que omitam a conversão final; ou seja, que retornemu8
. Eu usaria "Number" como sufixo para indicar que o valor de retorno é um número, adequado para uso em cálculos. Você pode querer modificargetFile
egetRank
chamar essas novas funções para evitar repetir a aritmética.operator[]
não podem ser definidos fora de uma classe. Então, minha solução seria definir uma classe para encapsular seu array. Você pode descobrir que isso também traz outros benefícios, como a possibilidade de moverprecomputeSquareDistance
para o construtor. E não apenas um construtor, mas umconstexpr
construtor (os valores podem ser computados durante a compilação em vez de em tempo de execução).Em conjunto, essas mudanças reduzem as ocorrências de
static_cast
no seu código de demonstração de 25 para 10. O ponto principal é que, além de uma conversão namain
função, as conversões foram movidas para funções utilitárias e constantes. As utilidades são o que é usado repetidamente, nãostatic_cast
diretamente. Pode haver mais utilidades necessárias no seu código real, mas pode não ser tão ruim quanto você pensava. Principalmente se você parar de dificultar as coisas para si mesmo (veja o nº 3).Reconheço a resposta sobre o uso de enumerações sem escopo dentro de um namespace como outra abordagem razoável. Certamente vale a pena considerar se você não conseguir mover conversões suficientes do seu código real para funções utilitárias.
Para referência, a função principal após a introdução do acima:
O
static_cast
que permanece nesta função é um artefato do usouint8_t
e não está relacionado ao uso de enumerações com escopo.Como @NathanOliver sugeriu nos comentários acima, use enumerações sem escopo dentro de um namespace. Valores de enumerações sem escopo podem ser promovidos ou convertidos para tipos integrais. O contrário pode ser feito usando
static_cast
.Com isso em mente, você pode fazer o seguinte:
Acho que isso deve cobrir a ideia. Lembre-se de que a promoção e a conversão para o tipo integral são implícitas. A conversão para um valor enum pode ser feita explicitamente usando a
static_cast
(desde que não exceda o intervalo de valores, UB caso contrário).use função auxiliar ou macros: defina função utilitária para encapsular a conversão:
cpp
constexpr unit8_t to_unit8(valor enumClass)