23 Stimmen

Typsichere(r) Bitflags in C++?

Bei der Überarbeitung eines alten C++-Codes bin ich auf mehrere Bitflags als Enums definiert.

enum FooFlags
{
    FooFlag1 = 1 << 0,
    FooFlag2 = 1 << 1,
    FooFlag3 = 1 << 2
    // etc...
};

Das ist nicht ungewöhnlich, aber es hat mich gestört, dass die Typinformationen verloren gehen, sobald man beginnt, Flaggen zu kombinieren.

int flags = FooFlag1 | FooFlag2;   // We've lost the information that this is a set of flags relating to *Foo*

Eine Suche auf SO hat ergeben, dass ich nicht der nur eine Das stört mich.

Eine Alternative ist es, Flags als #defines oder const integrals zu deklarieren, so dass bitweise Operationen den Typ (wahrscheinlich) nicht verändern würden. Das Problem dabei ist, dass sich unser Bitset mit nicht verwandten Flags über Ints oder andere Enums vermischen kann.

Ich bin vertraut mit std::bitset y boost::dynamic_bitset aber keines von beiden ist auf mein Problem zugeschnitten. Was ich suche, ist etwas wie C#s FlaggenAttribut .

Meine Frage ist, welche anderen Lösungen gibt es für eine (mehr) Typ sicher Satz von Bitflags?

Ich werde meine eigene Lösung weiter unten veröffentlichen.

21voto

Sie können Operatoren für Aufzählungstypen überladen, die das richtige typisierte Ergebnis zurückgeben.

inline FooFlags operator|(FooFlags a, FooFlags b) {
  return static_cast<FooFlags>(+a | +b);
}

Um theoretisch sicher zu sein, sollten Sie manuell den höchstmöglichen Wert deklarieren, damit der Bereich des Aufzählungstyps garantiert alle Kombinationen erfasst.

  • Das ist eigentlich nicht nötig: Der Bereich einer Aufzählung wird immer in der Lage sein, alle Kombinationen zu erfassen, da der höchste positive Wert des Bereichs einer Aufzählung immer (2^N)-1 für die erste N den höchsten Zählerwert darstellen kann. Dieser Wert hat alle Bits 1.

14voto

luke Punkte 34409

Hier ist meine eigene Lösung, die Elemente von C++0x verwendet, die die aktuelle Version von VS2010 ermöglicht:

#include <iostream>
#include <numeric>
#include <string>

#include <initializer_list>

template <typename enumT>
class FlagSet
{
    public:

        typedef enumT                     enum_type;
        typedef decltype(enumT()|enumT()) store_type;

        // Default constructor (all 0s)
        FlagSet() : FlagSet(store_type(0))
        {

        }

        // Initializer list constructor
        FlagSet(const std::initializer_list<enum_type>& initList)
        {
            // This line didn't work in the initializer list like I thought it would.  It seems to dislike the use of the lambda.  Forbidden, or a compiler bug?
            flags_ = std::accumulate(initList.begin(), initList.end(), store_type(0), [](enum_type x, enum_type y) { return x | y; })
        }

        // Value constructor
        explicit FlagSet(store_type value) : flags_(value)
        {

        }

        // Explicit conversion operator
        operator store_type() const
        {
            return flags_;
        }

        operator std::string() const
        {
            return to_string();
        }

        bool operator [] (enum_type flag) const
        {
            return test(flag);
        }

        std::string to_string() const
        {
            std::string str(size(), '0');

            for(size_t x = 0; x < size(); ++x)
            {
                str[size()-x-1] = (flags_ & (1<<x) ? '1' : '0');
            }

            return str;
        }

        FlagSet& set()
        {
            flags_ = ~store_type(0);
            return *this;
        }

        FlagSet& set(enum_type flag, bool val = true)
        {
            flags_ = (val ? (flags_|flag) : (flags_&~flag));
            return *this;
        }

        FlagSet& reset()
        {
            flags_ = store_type(0);
            return *this;
        }

        FlagSet& reset(enum_type flag)
        {
            flags_ &= ~flag;
            return *this;
        }

        FlagSet& flip()
        {
            flags_ = ~flags_;
            return *this;
        }

        FlagSet& flip(enum_type flag)
        {
            flags_ ^= flag;
            return *this;
        }

        size_t count() const
        {
            // http://www-graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan

            store_type bits = flags_;
            size_t total = 0;
            for (; bits != 0; ++total)
            {
                bits &= bits - 1; // clear the least significant bit set
            }
            return total;
        }

        /*constexpr*/ size_t size() const   // constexpr not supported in vs2010 yet
        {
            return sizeof(enum_type)*8;
        }

        bool test(enum_type flag) const
        {
            return (flags_ & flag) > 0;
        }

        bool any() const
        {
            return flags_ > 0;
        }

        bool none() const
        {
            return flags == 0;
        }

    private:

        store_type flags_;

};

template<typename enumT>
FlagSet<enumT> operator & (const FlagSet<enumT>& lhs, const FlagSet<enumT>& rhs)
{
    return FlagSet<enumT>(FlagSet<enumT>::store_type(lhs) & FlagSet<enumT>::store_type(rhs));
}

template<typename enumT>
FlagSet<enumT> operator | (const FlagSet<enumT>& lhs, const FlagSet<enumT>& rhs)
{
    return FlagSet<enumT>(FlagSet<enumT>::store_type(lhs) | FlagSet<enumT>::store_type(rhs));
}

template<typename enumT>
FlagSet<enumT> operator ^ (const FlagSet<enumT>& lhs, const FlagSet<enumT>& rhs)
{
    return FlagSet<enumT>(FlagSet<enumT>::store_type(lhs) ^ FlagSet<enumT>::store_type(rhs));
}

template <class charT, class traits, typename enumT>
std::basic_ostream<charT, traits> & operator << (std::basic_ostream<charT, traits>& os, const FlagSet<enumT>& flagSet)
{
    return os << flagSet.to_string();
}

Die Schnittstelle ist modelliert nach std::bitset . Mein Ziel war es, dem C++-Ethos der Typsicherheit und des minimalen (wenn überhaupt) Overheads treu zu bleiben. Ich würde mich über jede Rückmeldung zu meiner Implementierung freuen.

Hier ist ein kleines Beispiel:

#include <iostream>

enum KeyMod
{
    Alt     = 1 << 0,  // 1
    Shift   = 1 << 1,  // 2
    Control = 1 << 2   // 4
};

void printState(const FlagSet<KeyMod>& keyMods)
{
    std::cout << "Alt is "     << (keyMods.test(Alt)     ? "set" : "unset") << ".\n";
    std::cout << "Shift is "   << (keyMods.test(Shift)   ? "set" : "unset") << ".\n";
    std::cout << "Control is " << (keyMods.test(Control) ? "set" : "unset") << ".\n";
}

int main(int argc, char* argv[])
{
    FlagSet<KeyMod> keyMods(Shift | Control);

    printState(keyMods);

    keyMods.set(Alt);
    //keyMods.set(24);    // error - an int is not a KeyMod value
    keyMods.set(Shift);
    keyMods.flip(Control);

    printState(keyMods);

    return 0;
}

8voto

Christian Punkte 888

Ich dachte, ich könnte eine C++11-Version für enum class

FooFlags operator|(FooFlags a, FooFlags b)
{
  typedef std::underlying_type<FooFlags>::type enum_type;
  return static_cast<FooFlags>(static_cast<enum_type>(a) | static_cast<enum_type>(b));
}

Wenn Sie c++11 Version unterstützt es ich denke, dies wäre ein erstklassiger Kandidat für constexpr

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X