Quels sont les avantages/inconvénients de l'utilisation des bits par rapport aux indicateurs enum?
namespace Flag {
enum State {
Read = 1 << 0,
Write = 1 << 1,
Binary = 1 << 2,
};
}
namespace Plain {
enum State {
Read,
Write,
Binary,
Count
};
}
int main()
{
{
unsigned int state = Flag::Read | Flag::Binary;
std::cout << state << std::endl;
state |= Flag::Write;
state &= ~(Flag::Read | Flag::Binary);
std::cout << state << std::endl;
} {
std::bitset<Plain::Count> state;
state.set(Plain::Read);
state.set(Plain::Binary);
std::cout << state.to_ulong() << std::endl;
state.flip();
std::cout << state.to_ulong() << std::endl;
}
return 0;
}
Comme je peux le constater jusqu'à présent, les ensembles de bits ont des fonctions plus pratiques pour régler/effacer/retourner, mais l'utilisation d'enum-flags est une approche plus répandue.
Quels sont les inconvénients possibles des bits et quoi et quand dois-je utiliser dans mon code quotidien?
Compilez-vous avec l'optimisation sur? Il est très peu probable qu'il existe un facteur de vitesse 24x.
Pour moi, bitset est supérieur, car il gère l'espace pour vous:
int
/long long
.unsigned char
/unsigned short
- je ne suis pas sûr que les implémentations appliquent cette optimisation)std::bitset
et c-style enum
présentent des inconvénients importants pour la gestion des indicateurs. Tout d'abord, considérons l'exemple de code suivant:
namespace Flag {
enum State {
Read = 1 << 0,
Write = 1 << 1,
Binary = 1 << 2,
};
}
namespace Plain {
enum State {
Read,
Write,
Binary,
Count
};
}
void f(int);
void g(int);
void g(Flag::State);
void h(std::bitset<sizeof(Flag::State)>);
namespace system1 {
Flag::State getFlags();
}
namespace system2 {
Plain::State getFlags();
}
int main()
{
f(Flag::Read); // Flag::Read is implicitly converted to `int`, losing type safety
f(Plain::Read); // Plain::Read is also implicitly converted to `int`
auto state = Flag::Read | Flag::Write; // type is not `Flag::State` as one could expect, it is `int` instead
g(state); // This function calls the `int` overload rather than the `Flag::State` overload
auto system1State = system1::getFlags();
auto system2State = system2::getFlags();
if (system1State == system2State) {} // Compiles properly, but semantics are broken, `Flag::State`
std::bitset<sizeof(Flag::State)> flagSet; // Notice that the type of bitset only indicates the amount of bits, there's no type safety here either
std::bitset<sizeof(Plain::State)> plainSet;
// f(flagSet); bitset doesn't implicitly convert to `int`, so this wouldn't compile which is slightly better than c-style `enum`
flagSet.set(Flag::Read); // No type safety, which means that bitset
flagSet.reset(Plain::Read); // is willing to accept values from any enumeration
h(flagSet); // Both kinds of sets can be
h(plainSet); // passed to the same function
}
Même si vous pensez peut-être que ces problèmes sont faciles à déceler sur des exemples simples, ils finissent par apparaître dans toutes les bases de code qui génèrent des indicateurs au-dessus des variables enum
et std::bitset
de style c.
Alors, que pouvez-vous faire pour une meilleure sécurité de type? Premièrement, l'énumération étendue de C++ 11 constitue une amélioration pour la sécurité des types. Mais cela nuit beaucoup à la commodité. Une partie de la solution consiste à utiliser des opérateurs au niveau des bits générés par des modèles pour des énumérations étendues. Voici un excellent article de blog qui explique son fonctionnement et fournit un code fonctionnel: https://www.justsoftwaresolutions.co.uk/cplusplus/using-enum-classes-as-bitfields.html
Voyons maintenant à quoi cela ressemblerait:
enum class FlagState {
Read = 1 << 0,
Write = 1 << 1,
Binary = 1 << 2,
};
template<>
struct enable_bitmask_operators<FlagState>{
static const bool enable=true;
};
enum class PlainState {
Read,
Write,
Binary,
Count
};
void f(int);
void g(int);
void g(FlagState);
FlagState h();
namespace system1 {
FlagState getFlags();
}
namespace system2 {
PlainState getFlags();
}
int main()
{
f(FlagState::Read); // Compile error, FlagState is not an `int`
f(PlainState::Read); // Compile error, PlainState is not an `int`
auto state = Flag::Read | Flag::Write; // type is `FlagState` as one could expect
g(state); // This function calls the `FlagState` overload
auto system1State = system1::getFlags();
auto system2State = system2::getFlags();
if (system1State == system2State) {} // Compile error, there is no `operator==(FlagState, PlainState)`
auto someFlag = h();
if (someFlag == FlagState::Read) {} // This compiles fine, but this is another type of recurring bug
}
La dernière ligne de cet exemple montre un problème qui ne peut toujours pas être résolu au moment de la compilation. Dans certains cas, la comparaison pour l'égalité peut être ce qui est vraiment souhaité. Mais la plupart du temps, il s’agit en réalité de if ((someFlag & FlagState::Read) == FlagState::Read)
.
Afin de résoudre ce problème, nous devons différencier le type d'un énumérateur du type d'un masque de bits. Voici un article qui détaille une amélioration de la solution partielle à laquelle j'ai fait référence précédemment: https://dalzhim.github.io/2017/08/11/Improving-the-enum-class-bitmask/ : Je suis l'auteur de cet article ultérieur.
Lorsque vous utilisez les opérateurs au niveau des bits générés par les modèles du dernier article, vous bénéficiez de tous les avantages que nous avons démontrés dans le dernier morceau de code, tout en interceptant le bogue mask == enumerator
.
(Mode annonce activé) Vous pouvez obtenir les deux: une interface pratique et des performances maximales. Et la sécurité de type aussi. https://github.com/oliora/bitmask