Как использовать перечисления в качестве флагов в C ++? - PullRequest
162 голосов
/ 19 сентября 2009

Обработка enum s как флагов прекрасно работает в C # через атрибут [Flags], но как лучше всего это сделать в C ++?

Например, я хотел бы написать:

enum AnimalFlags
{
    HasClaws = 1,
    CanFly =2,
    EatsFish = 4,
    Endangered = 8
};

seahawk.flags = CanFly | EatsFish | Endangered;

Однако я получаю ошибки компилятора относительно int / enum преобразований. Есть ли лучший способ выразить это, чем просто тупое литье? Предпочтительно, я не хочу полагаться на конструкции из сторонних библиотек, таких как boost или Qt.

РЕДАКТИРОВАТЬ: Как указано в ответах, я могу избежать ошибки компилятора, объявив seahawk.flags как int. Тем не менее, я хотел бы иметь некоторый механизм для обеспечения безопасности типов, поэтому кто-то не может написать seahawk.flags = HasMaximizeButton.

Ответы [ 19 ]

218 голосов
/ 19 сентября 2009

«Правильный» способ - определить битовые операторы для перечисления, как:

enum AnimalFlags
{
    HasClaws = 1,
    CanFly =2,
    EatsFish = 4,
    Endangered = 8
};

inline AnimalFlags operator|(AnimalFlags a, AnimalFlags b)
{return static_cast<AnimalFlags>(static_cast<int>(a) | static_cast<int>(b));}

Etc. Остальные битовые операторы. При необходимости измените, если диапазон enum превышает int.

114 голосов
/ 06 апреля 2012

Примечание (также немного не по теме): еще один способ сделать уникальные флаги можно сделать с помощью сдвига битов. Мне самому легче читать это.

enum Flags
{
    A = 1 << 0, // binary 0001
    B = 1 << 1, // binary 0010
    C = 1 << 2, // binary 0100
    D = 1 << 3, // binary 1000
};

Может содержать значения вплоть до целого числа, то есть, в большинстве случаев, 32 флага, что четко отражается в величине смещения.

51 голосов
/ 18 апреля 2014

Для таких ленивых, как я, есть шаблонное решение для копирования и вставки:

template<class T> inline T operator~ (T a) { return (T)~(int)a; }
template<class T> inline T operator| (T a, T b) { return (T)((int)a | (int)b); }
template<class T> inline T operator& (T a, T b) { return (T)((int)a & (int)b); }
template<class T> inline T operator^ (T a, T b) { return (T)((int)a ^ (int)b); }
template<class T> inline T& operator|= (T& a, T b) { return (T&)((int&)a |= (int)b); }
template<class T> inline T& operator&= (T& a, T b) { return (T&)((int&)a &= (int)b); }
template<class T> inline T& operator^= (T& a, T b) { return (T&)((int&)a ^= (int)b); }
40 голосов
/ 19 сентября 2009

Какого типа переменная seahawk.flags?

В стандартном C ++ перечисления не являются типобезопасными. Они фактически целые числа.

AnimalFlags НЕ должен быть типом вашей переменной, ваша переменная должна быть int и ошибка исчезнет.

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

Значения перечисления ARE типа int по умолчанию. Таким образом, вы можете, конечно, побитовый ИЛИ объединить их и собрать их вместе и сохранить результат в int.

Тип enum является ограниченным подмножеством int, значение которого является одним из перечисляемых значений. Следовательно, когда вы создаете какое-то новое значение за пределами этого диапазона, вы не можете присвоить его без приведения к переменной вашего типа enum.

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

РЕДАКТИРОВАТЬ: Автор сказал, что они были заинтересованы в безопасности типов и они не хотят значения, которое не должно существовать внутри типа int.

Но было бы небезопасно помещать значение вне диапазона AnimalFlags внутри переменной типа AnimalFlags.

Существует безопасный способ проверки значений вне диапазона, хотя внутри типа int ...

int iFlags = HasClaws | CanFly;
//InvalidAnimalFlagMaxValue-1 gives you a value of all the bits 
// smaller than itself set to 1
//This check makes sure that no other bits are set.
assert(iFlags & ~(InvalidAnimalFlagMaxValue-1) == 0);

enum AnimalFlags {
    HasClaws = 1,
    CanFly =2,
    EatsFish = 4,
    Endangered = 8,

    // put new enum values above here
    InvalidAnimalFlagMaxValue = 16
};

Вышеприведенное не останавливает вас от установки недопустимого флага из другого перечисления со значением 1,2,4 или 8.

Если вы хотите абсолютной безопасности типов, тогда вы можете просто создать std :: set и сохранить каждый флаг внутри себя. Это не экономит место, но является безопасным типом и дает вам те же возможности, что и битовый флаг int.

C ++ 0x примечание: строго типизированные перечисления

В C ++ 0x вы, наконец, можете получить безопасные для типов значения enum ....

enum class AnimalFlags {
    CanFly = 2,
    HasClaws = 4
};

if(CanFly == 2) { }//Compiling error
37 голосов
/ 25 октября 2013

Обратите внимание, если вы работаете в среде Windows, в winnt.h определен макрос DEFINE_ENUM_FLAG_OPERATORS, который выполняет эту работу за вас. Так что в этом случае вы можете сделать это:

enum AnimalFlags
{
    HasClaws = 1,
    CanFly =2,
    EatsFish = 4,
    Endangered = 8
};
DEFINE_ENUM_FLAG_OPERATORS(AnimalFlags)

seahawk.flags = CanFly | EatsFish | Endangered;
22 голосов
/ 28 ноября 2015

Я считаю, что принятый в настоящее время ответ по eidolon слишком опасен. Оптимизатор компилятора может делать предположения о возможных значениях в перечислении, и вы можете получить мусор обратно с недопустимыми значениями. И обычно никто не хочет определять все возможные перестановки в перечислениях флагов.

Как говорит Брайан Р. Бонди ниже, если вы используете C ++ 11 (что все должны, это хорошо), теперь вы можете сделать это проще с помощью enum class:

enum class ObjectType : uint32_t
{
    ANIMAL = (1 << 0),
    VEGETABLE = (1 << 1),
    MINERAL = (1 << 2)
};


constexpr enum ObjectType operator |( const enum ObjectType selfValue, const enum ObjectType inValue )
{
    return (enum ObjectType)(uint32_t(selfValue) | uint32_t(inValue));
}

// ... add more operators here. 

Это обеспечивает стабильный диапазон размеров и значений путем указания типа перечисления, запрещает автоматическое преобразование перечислений в целочисленные значения и т. Д. С помощью enum class и использует constexpr, чтобы гарантировать, что код для операторов будет встроен и, таким образом так же быстро, как обычные числа.

Для людей, придерживающихся диалектов C ++ до 11 лет

Если бы я застрял с компилятором, который не поддерживает C ++ 11, я бы пошел с переносом типа int в класс, который затем разрешает использовать только побитовые операторы и типы из этого перечисления для установки его значения:

template<class ENUM,class UNDERLYING=typename std::underlying_type<ENUM>::type>
class SafeEnum
{
public:
    SafeEnum() : mFlags(0) {}
    SafeEnum( ENUM singleFlag ) : mFlags(singleFlag) {}
    SafeEnum( const SafeEnum& original ) : mFlags(original.mFlags) {}

    SafeEnum&   operator |=( ENUM addValue )    { mFlags |= addValue; return *this; }
    SafeEnum    operator |( ENUM addValue )     { SafeEnum  result(*this); result |= addValue; return result; }
    SafeEnum&   operator &=( ENUM maskValue )   { mFlags &= maskValue; return *this; }
    SafeEnum    operator &( ENUM maskValue )    { SafeEnum  result(*this); result &= maskValue; return result; }
    SafeEnum    operator ~()    { SafeEnum  result(*this); result.mFlags = ~result.mFlags; return result; }
    explicit operator bool()                    { return mFlags != 0; }

protected:
    UNDERLYING  mFlags;
};

Вы можете определить это почти как обычный enum + typedef:

enum TFlags_
{
    EFlagsNone  = 0,
    EFlagOne    = (1 << 0),
    EFlagTwo    = (1 << 1),
    EFlagThree  = (1 << 2),
    EFlagFour   = (1 << 3)
};

typedef SafeEnum<enum TFlags_>  TFlags;

И использование также похоже:

TFlags      myFlags;

myFlags |= EFlagTwo;
myFlags |= EFlagThree;

if( myFlags & EFlagTwo )
    std::cout << "flag 2 is set" << std::endl;
if( (myFlags & EFlagFour) == EFlagsNone )
    std::cout << "flag 4 is not set" << std::endl;

И вы также можете переопределить базовый тип для бинарно-стабильных перечислений (как в C ++ 11 enum foo : type), используя второй параметр шаблона, то есть typedef SafeEnum<enum TFlags_,uint8_t> TFlags;.

Я пометил переопределение operator bool ключевым словом explicit в C ++ 11, чтобы оно не приводило к преобразованиям int, поскольку это может привести к тому, что наборы флагов в конечном итоге свернуты в 0 или 1 при их записи. Если вы не можете использовать C ++ 11, не используйте эту перегрузку и переписайте первое условное выражение в примере использования как (myFlags & EFlagTwo) == EFlagTwo.

17 голосов
/ 19 сентября 2009

Самый простой способ сделать это, как показано здесь , используя стандартный класс библиотеки bitset .

Чтобы эмулировать функцию C # безопасным для типов способом, вам нужно написать оболочку шаблона вокруг набора битов, заменив аргументы int перечислением, заданным в качестве параметра типа для шаблона. Что-то вроде:

    template <class T, int N>
class FlagSet
{

    bitset<N> bits;

    FlagSet(T enumVal)
    {
        bits.set(enumVal);
    }

    // etc.
};

enum MyFlags
{
    FLAG_ONE,
    FLAG_TWO
};

FlagSet<MyFlags, 2> myFlag;
11 голосов
/ 27 февраля 2016

На мой взгляд, ни один из ответов до сих пор не идеален. Чтобы быть идеальным, я ожидал бы решения:

  1. Поддержка операторов ==, !=, =, &, &=, |, |= и ~ в обычных смысл (то есть a & b)
  2. Будьте безопасны по типу, т. Е. Не разрешайте присваивать неперечисляемые значения, такие как литералы или целочисленные типы (за исключением побитовых комбинаций перечисляемых значений), или разрешайте присваивать переменную перечисления целочисленному типу
  3. Допустимые выражения, такие как if (a & b)...
  4. Не требует злых макросов, специфических функций реализации или других хаков

Большинство решений на данный момент относятся к пунктам 2 или 3. На мой взгляд, WebDancer - закрытие, но оно не выполняется в точке 3 и должно повторяться для каждого перечисления.

Мое предлагаемое решение - это обобщенная версия WebDancer, которая также касается пункта 3:

#include <cstdint>
#include <type_traits>

template<typename T = typename std::enable_if<std::is_enum<T>::value, T>::type>
class auto_bool
{
    T val_;
public:
    constexpr auto_bool(T val) : val_(val) {}
    constexpr operator T() const { return val_; }
    constexpr explicit operator bool() const
    {
        return static_cast<std::underlying_type_t<T>>(val_) != 0;
    }
};

template <typename T = typename std::enable_if<std::is_enum<T>::value, T>::type>
constexpr auto_bool<T> operator&(T lhs, T rhs)
{
    return static_cast<T>(
        static_cast<typename std::underlying_type<T>::type>(lhs) &
        static_cast<typename std::underlying_type<T>::type>(rhs));
}

template <typename T = typename std::enable_if<std::is_enum<T>::value, T>::type>
constexpr T operator|(T lhs, T rhs)
{
    return static_cast<T>(
        static_cast<typename std::underlying_type<T>::type>(lhs) |
        static_cast<typename std::underlying_type<T>::type>(rhs));
}

enum class AnimalFlags : uint8_t 
{
    HasClaws = 1,
    CanFly = 2,
    EatsFish = 4,
    Endangered = 8
};

enum class PlantFlags : uint8_t
{
    HasLeaves = 1,
    HasFlowers = 2,
    HasFruit = 4,
    HasThorns = 8
};

int main()
{
    AnimalFlags seahawk = AnimalFlags::CanFly;        // Compiles, as expected
    AnimalFlags lion = AnimalFlags::HasClaws;         // Compiles, as expected
    PlantFlags rose = PlantFlags::HasFlowers;         // Compiles, as expected
//  rose = 1;                                         // Won't compile, as expected
    if (seahawk != lion) {}                           // Compiles, as expected
//  if (seahawk == rose) {}                           // Won't compile, as expected
//  seahawk = PlantFlags::HasThorns;                  // Won't compile, as expected
    seahawk = seahawk | AnimalFlags::EatsFish;        // Compiles, as expected
    lion = AnimalFlags::HasClaws |                    // Compiles, as expected
           AnimalFlags::Endangered;
//  int eagle = AnimalFlags::CanFly |                 // Won't compile, as expected
//              AnimalFlags::HasClaws;
//  int has_claws = seahawk & AnimalFlags::CanFly;    // Won't compile, as expected
    if (seahawk & AnimalFlags::CanFly) {}             // Compiles, as expected
    seahawk = seahawk & AnimalFlags::CanFly;          // Compiles, as expected

    return 0;
}

Это создает перегрузки необходимых операторов, но использует SFINAE, чтобы ограничить их перечисляемыми типами. Обратите внимание, что в интересах краткости я не определил все операторы, но единственный, который отличается, это &. В настоящее время операторы являются глобальными (т.е. применяются ко всем перечисляемым типам), но это можно уменьшить либо путем помещения перегрузок в пространство имен (что я делаю), либо путем добавления дополнительных условий SFINAE (возможно, с использованием определенных базовых типов или специально созданных псевдонимов типов). ). underlying_type_t - это функция C ++ 14, но, похоже, она хорошо поддерживается и ее легко эмулировать для C ++ 11 с помощью простого template<typename T> using underlying_type_t = underlying_type<T>::type;

6 голосов
/ 11 декабря 2015

Стандарт C ++ прямо говорит об этом, см. Раздел «17.5.2.1.3 Типы битовых масок»:

http://www.open -std.org / ОТК1 / SC22 / wg21 / документы / документы / 2012 / n3485.pdf

Получив этот "шаблон", вы получите:

enum AnimalFlags : unsigned int
{
    HasClaws = 1,
    CanFly = 2,
    EatsFish = 4,
    Endangered = 8
};

constexpr AnimalFlags operator|(AnimalFlags X, AnimalFlags Y) {
    return static_cast<AnimalFlags>(
        static_cast<unsigned int>(X) | static_cast<unsigned int>(Y));
}

AnimalFlags& operator|=(AnimalFlags& X, AnimalFlags Y) {
    X = X | Y; return X;
}

И аналогично для других операторов. Также обратите внимание на «constexpr», он необходим, если вы хотите, чтобы компилятор мог выполнять операторы во время компиляции.

Если вы используете C ++ / CLI и хотите иметь возможность присваивать перечислимым членам ref-классов, вам нужно вместо этого использовать отслеживание ссылок:

AnimalFlags% operator|=(AnimalFlags% X, AnimalFlags Y) {
    X = X | Y; return X;
}

ПРИМЕЧАНИЕ. Этот пример неполон, полный раздел операторов см. В разделе «17.5.2.1.3 Типы битовых масок».

6 голосов
/ 09 августа 2015

Я обнаружил, что задаю тот же вопрос, и придумал общее решение на основе C ++ 11, подобное сору:

template <typename TENUM>
class FlagSet {

private:
    using TUNDER = typename std::underlying_type<TENUM>::type;
    std::bitset<std::numeric_limits<TUNDER>::max()> m_flags;

public:
    FlagSet() = default;

    template <typename... ARGS>
    FlagSet(TENUM f, ARGS... args) : FlagSet(args...)
    {   
        set(f);
    }   
    FlagSet& set(TENUM f)
    {   
        m_flags.set(static_cast<TUNDER>(f));
        return *this;
    }   
    bool test(TENUM f)
    {   
        return m_flags.test(static_cast<TUNDER>(f));
    }   
    FlagSet& operator|=(TENUM f)
    {   
        return set(f);
    }   
};

Интерфейс может быть улучшен по вкусу. Тогда его можно использовать так:

FlagSet<Flags> flags{Flags::FLAG_A, Flags::FLAG_C};
flags |= Flags::FLAG_D;
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...