Was sind Vor-/Nachteile von Verwendungs-Bitsets über Enum-Flags?
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;
}
Wie ich bisher sehen kann, verfügen Bitsets über günstigere Set/Clear/Flip-Funktionen, aber die Verwendung von Enum-Flags ist ein weit verbreiteter Ansatz.
Was sind mögliche Nachteile von Bitsets und was sollte ich in meinem täglichen Code verwenden?
Kompilierst du mit der Optimierung weiter? Es ist sehr unwahrscheinlich, dass es einen 24-fachen Geschwindigkeitsfaktor gibt.
Für mich ist Bitset überlegen, weil es Platz für Sie schafft:
int
/long long
zu wenig Speicherplatz geben.unsigned char
/unsigned short
passen - ich bin nicht sicher, dass Implementierungen diese Optimierung anwenden)Sowohl std::bitset
als auch c-style enum
haben wichtige Nachteile für die Verwaltung von Flags. Betrachten wir zunächst den folgenden Beispielcode:
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
}
Auch wenn Sie denken, dass diese Probleme in einfachen Beispielen leicht zu erkennen sind, schleichen sie sich in jeder Codebasis, die Flags auf c-style enum
und std::bitset
aufbaut.
Was können Sie also für eine bessere Typsicherheit tun? Erstens ist die Aufzählung von C++ 11 eine Verbesserung der Typsicherheit. Aber es behindert die Bequemlichkeit sehr. Ein Teil der Lösung besteht in der Verwendung von durch Vorlagen generierten bitweisen Operatoren für Bereichsummen. Hier ist ein großartiger Blogbeitrag, der erklärt, wie es funktioniert und außerdem Funktionscode enthält: https://www.justsoftwaresolutions.co.uk/cplusplus/using-enum-classes-as-bitfields.html
Nun wollen wir mal sehen, wie das aussehen würde:
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
}
Die letzte Zeile dieses Beispiels zeigt ein Problem, das zum Zeitpunkt des Kompilierens noch nicht erkannt werden kann. In einigen Fällen kann der Vergleich auf Gleichheit das sein, was wirklich gewünscht wird. Meistens ist jedoch if ((someFlag & FlagState::Read) == FlagState::Read)
gemeint.
Um dieses Problem zu lösen, müssen wir den Typ eines Enumerators vom Typ einer Bitmaske unterscheiden. Hier ist ein Artikel, der eine Verbesserung der zuvor genannten Teillösung beschreibt: https://dalzhim.github.io/2017/08/11/Improving-the-enum-class-bitmask/ Disclaimer : Ich bin der Autor dieses späteren Artikels.
Wenn Sie die durch die Vorlage generierten bitweisen Operatoren aus dem letzten Artikel verwenden, erhalten Sie alle Vorteile, die wir im letzten Teil des Codes gezeigt haben, während Sie auch den Code mask == enumerator
abfangen.
(Anzeigenmodus aktiviert) Sie können beides erhalten: eine komfortable Benutzeroberfläche und maximale Leistung. Und auch die Typsicherheit. https://github.com/oliora/bitmask