Введите безопасные (r) битовые флаги в С++?

Пересматривая старый код на C++, я наткнулся на несколько битовых флагов, определенных как перечисления.

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

Это не редкость, но меня беспокоило, что как только вы начинаете комбинировать флаги, вы теряете информацию о типе.

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

Некоторый поиск на SO показал, что я не только one обеспокоен этим.

Одна из альтернатив — объявить флаги как #define или константные интегралы, чтобы побитовые операции не преобразовывали тип (вероятно). Проблема в том, что это позволяет нашему набору битов смешиваться с несвязанными флагами через целые числа или другие перечисления.

Я знаком с std::bitset и boost::dynamic_bitset, но ни один из них не предназначен для решения моей проблемы. Я ищу что-то вроде FlagsAttribute С#.

Мой вопрос в том, какие другие решения существуют для (более) безопасного набора битовых флагов?

Ниже я опубликую собственное решение.


person luke    schedule 19.11.2010    source источник


Ответы (3)


Вы можете перегружать операторы для типов перечисления, которые возвращают правильный типизированный результат.

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

Следует отметить, что для теоретической безопасности следует вручную объявить максимально возможное значение, чтобы диапазон типа перечисления гарантированно перехватывал все комбинации.

  • На самом деле это не нужно: диапазон перечисления всегда сможет уловить все комбинации, потому что наибольшее положительное значение диапазона перечисления всегда равно (2^N)-1, поскольку первое N может представлять самый высокий перечислитель. Это значение имеет все биты 1.
person Johannes Schaub - litb    schedule 19.11.2010
comment
Почему +a и +b, а не просто a и b из интереса? Безопасность? - person Stuart Golodetz; 19.11.2010
comment
@sgolodetz это войдет в бесконечную рекурсию (путем вызова определяемого operator|). - person Johannes Schaub - litb; 19.11.2010
comment
@Johannes Schaub: Итак, вы используете знак + как неявное приведение к целому числу. Почему бы не сказать об этом явно и не использовать static_cast‹›()? - person Martin York; 19.11.2010
comment
@Martin, потому что я не знаю базового типа перечисления. Он может переполнить как long, так и unsigned long в зависимости от перечисления. См. stackoverflow.com/questions/75538/hidden-features. -из-c/ - person Johannes Schaub - litb; 19.11.2010
comment
@Martin, в качестве альтернативы вы можете написать ((a + 0) | (b + 0)), если хотите. - person Johannes Schaub - litb; 19.11.2010
comment
Наверное единственный раз видел + использовал в реальной ситуации. Прохладный. - person GManNickG; 20.11.2010
comment
@Johannes: Рад, что спросил, спасибо :) Я совершенно упустил суть! - person Stuart Golodetz; 20.11.2010
comment
@Zack, Нет, это не работает с enum class в С++ 11, поскольку он строго типизирован и не выполняет неявное приведение к целочисленному типу. - person Christian; 15.08.2012

Вот мое собственное решение, использующее элементы c++0x, которые позволяет текущая версия VS2010:

#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();
}

Интерфейс создан по образцу std::bitset. Моя цель состояла в том, чтобы быть верным идее С++ о безопасности типов и минимальных (если таковые имеются) накладных расходах. Буду рад любым отзывам о моей реализации.

Вот минимальный пример:

#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;
}
person luke    schedule 19.11.2010
comment
Есть ли шанс использовать пример для вашей реализации? - person Eric; 15.03.2013
comment
@ Эрик, мне кажется, это будет довольно прямолинейно. Что именно вы ищете? - person luke; 23.03.2013
comment
Простой пример объявления перечисления E, создания экземпляра FlagSet<E> и его использования. Конечно, я мог бы решить это, но пример сделал бы этот ответ лучше. - person Eric; 23.03.2013
comment
Qt имеет шаблон класса QFlags‹T›, который оборачивает перечисление безопасным способом: qt-project.org/doc/qt-5.0/qtcore/qflags.html. Он очень похож на этот, но больше похож на старые простые перечисления флагов, т. е. предпочитает перегруженные операторы методам. - person Oberon; 24.03.2013

Думал, я мог бы добавить версию С++ 11 для 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));
}

Если ваша версия С++ 11 поддерживает это, я думаю, это будет главный кандидат на constexpr

person Christian    schedule 15.08.2012