it-swarm.com.de

C++ - Enum-Flags vs. Bitset

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?

15

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:

  • kann beliebig erweitert werden. Wenn Sie viele Flags haben, kann es in der Version int/long long zu wenig Speicherplatz geben.
  • kann weniger Speicherplatz beanspruchen, wenn Sie nur mehrere Flags verwenden (es kann in einen unsigned char/unsigned short passen - ich bin nicht sicher, dass Implementierungen diese Optimierung anwenden)
2
geza

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.

4
Dalzhim

(Anzeigenmodus aktiviert) Sie können beides erhalten: eine komfortable Benutzeroberfläche und maximale Leistung. Und auch die Typsicherheit. https://github.com/oliora/bitmask

0
oliora