Использование перечисляемых областей для битовых флагов в C ++

60

enum X : int(С #) или enum class X : int(C ++ , 11) представляет собой тип , который имеет скрытое внутреннее поле , intкоторый может содержать любое значение. Кроме того, Xв перечислении определен ряд предопределенных констант . Можно привести перечисление к его целочисленному значению и наоборот. Это все верно как в C #, так и в C ++ 11.

В C # перечисления используются не только для хранения отдельных значений, но и для хранения побитовых комбинаций флагов в соответствии с рекомендацией Microsoft . Такие перечисления (обычно, но не обязательно) украшены [Flags]атрибутом. Чтобы упростить жизнь разработчикам, побитовые операторы (OR, AND и т. Д.) Перегружены, так что вы можете легко сделать что-то вроде этого (C #):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Я опытный разработчик C #, но программирую на C ++ только пару дней, и я не знаком с соглашениями C ++. Я намерен использовать перечисление C ++ 11 точно так же, как я это делал в C #. В C ++ 11 побитовые операторы в перечисленных областях не перегружены, поэтому я хотел их перегрузить .

Это вызвало дебаты, и мнения, кажется, различаются между тремя вариантами:

  1. Переменная типа enum используется для хранения битового поля, аналогично C #:

    void M(NumericType flags);
    
    // With operator overloading:
    M(NumericType::Sign | NumericType::ZeroPadding);
    
    // Without operator overloading:
    M(static_cast<NumericType>(static_cast<int>(NumericType::Sign) | static_cast<int>(NumericType::ZeroPadding)));
    

    Но это противоречило бы строго типизированной философии перечисления в ограниченных перечислениях C ++ 11.

  2. Используйте простое целое число, если вы хотите сохранить побитовую комбинацию перечислений:

    void M(int flags);
    
    M(static_cast<int>(NumericType::Sign) | static_cast<int>(NumericType::ZeroPadding));
    

    Но это сведет все к a int, оставляя вас без понятия, какой тип вы должны использовать в методе.

  3. Напишите отдельный класс, который будет перегружать операторы и содержать побитовые флаги в скрытом целочисленном поле:

    class NumericTypeFlags {
        unsigned flags_;
    public:
        NumericTypeFlags () : flags_(0) {}
        NumericTypeFlags (NumericType t) : flags_(static_cast<unsigned>(t)) {}
        //...define BITWISE test/set operations
    };
    
    void M(NumericTypeFlags flags);
    
    M(NumericType::Sign | NumericType::ZeroPadding);
    

    ( Полный код по user315052 )

    Но тогда у вас нет IntelliSense или какой-либо другой поддержки, которая намекает вам на возможные значения.

Я знаю, что это субъективный вопрос , но: какой подход я должен использовать? Какой подход, если таковой имеется, наиболее широко известен в C ++? Какой подход вы используете при работе с битовыми полями и почему ?

Конечно, поскольку все три подхода работают, я ищу фактические и технические причины, общепринятые соглашения, а не просто личные предпочтения.

Например, из-за моего фона C # я склоняюсь к подходу 1 в C ++. Это дает дополнительное преимущество, заключающееся в том, что моя среда разработки может подсказывать мне возможные значения, а с перегруженными операторами enum это легко написать и понять, и довольно чисто. И подпись метода ясно показывает, какую ценность он ожидает. Но большинство людей здесь не согласны со мной, вероятно, по уважительной причине.

Даниэль А.А. Пельсмакер
источник
2
Комитет ISO C ++ счел вариант 1 достаточно важным, чтобы явно указать, что диапазон значений перечислений включает все двоичные комбинации флагов. (Это предшествует C ++ 03). Таким образом, существует объективное одобрение этого несколько субъективного вопроса.
MSalters
1
(Чтобы уточнить комментарий @MSalters, диапазон перечисления C ++ основан на его базовом типе (если это фиксированный тип), или иным образом на его перечислителях. В последнем случае диапазон основан на наименьшем битовом поле, которое может содержать все определенные перечислители ; например, для enum E { A = 1, B = 2, C = 4, };, диапазон составляет 0..7(3 бита). Таким образом, стандарт C ++ явно гарантирует, что # 1 всегда будет жизнеспособным вариантом. [В частности, по enum classумолчанию используется, enum class : intесли не указано иное, и, следовательно, всегда имеет фиксированный базовый тип.])
Джастин Тайм 2 Восстановить Монику

Ответы:

31

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

#include <type_traits>

enum class SBJFrameDrag
{
    None = 0x00,
    Top = 0x01,
    Left = 0x02,
    Bottom = 0x04,
    Right = 0x08,
};

inline SBJFrameDrag operator | (SBJFrameDrag lhs, SBJFrameDrag rhs)
{
    using T = std::underlying_type_t <SBJFrameDrag>;
    return static_cast<SBJFrameDrag>(static_cast<T>(lhs) | static_cast<T>(rhs));
}

inline SBJFrameDrag& operator |= (SBJFrameDrag& lhs, SBJFrameDrag rhs)
{
    lhs = lhs | rhs;
    return lhs;
}

(Обратите внимание, что type_traitsэто заголовок C ++ 11 и std::underlying_type_tфункция C ++ 14.)

Дейв
источник
6
std :: under_type_t - это C ++ 14. Может использовать std :: under_type <T> :: type в C ++ 11.
ddevienne
14
Почему вы используете static_cast<T>для ввода, но C-стиль приведен для результата здесь?
Руслан
2
@Ruslan Я задаю второй вопрос
audiFanatic
Почему вы даже беспокоитесь по поводу std ::sing_type_t, когда вы уже знаете, что это int?
poizan42
1
Если SBJFrameDragон определен в классе, а |оператор-впоследствии используется в определениях того же класса, как бы вы определили оператор так, чтобы его можно было использовать в классе?
Здравствуйте, до свидания
6

Исторически я всегда использовал старое (слабо типизированное) перечисление для именования битовых констант и просто явно использовал класс хранения для хранения результирующего флага. Здесь я должен был бы взять на себя ответственность за то, чтобы мои перечисления соответствовали типу хранилища, и отслеживать связь между полем и связанными с ним константами.

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

Например, предполагая побитовый или был перегружен:

enum class E1 { A=1, B=2, C=4 };
void test(E1 e) {
    switch(e) {
    case E1::A: do_a(); break;
    case E1::B: do_b(); break;
    case E1::C: do_c(); break;
    default:
        illegal_value();
    }
}
// ...
test(E1::A); // ok
test(E1::A | E1::B); // nope

Для третьего варианта вам понадобится шаблон для извлечения типа хранилища перечисления. Предполагая, что мы хотим форсировать неподписанный базовый тип (мы можем обработать и подписанный, с небольшим количеством кода):

template <size_t Size> struct IntegralTypeLookup;
template <> struct IntegralTypeLookup<sizeof(int64_t)> { typedef uint64_t Type; };
template <> struct IntegralTypeLookup<sizeof(int32_t)> { typedef uint32_t Type; };
template <> struct IntegralTypeLookup<sizeof(int16_t)> { typedef uint16_t Type; };
template <> struct IntegralTypeLookup<sizeof(int8_t)>  { typedef uint8_t Type; };

template <typename IntegralType> struct Integral {
    typedef typename IntegralTypeLookup<sizeof(IntegralType)>::Type Type;
};

template <typename ENUM> class EnumeratedFlags {
    typedef typename Integral<ENUM>::Type RawType;
    RawType raw;
public:
    EnumeratedFlags() : raw() {}
    EnumeratedFlags(EnumeratedFlags const&) = default;

    void set(ENUM e)   { raw |=  static_cast<RawType>(e); }
    void reset(ENUM e) { raw &= ~static_cast<RawType>(e); };
    bool test(ENUM e) const { return raw & static_cast<RawType>(e); }

    RawType raw_value() const { return raw; }
};
enum class E2: uint8_t { A=1, B=2, C=4 };
typedef EnumeratedFlags<E2> E2Flag;

Это все еще не дает вам IntelliSense или автозаполнения, но обнаружение типа хранилища менее уродливо, чем я первоначально ожидал.


Теперь я нашел альтернативу: вы можете указать тип хранилища для слабо типизированного перечисления. Он даже имеет тот же синтаксис, что и в C #

enum E4 : int { ... };

Поскольку он слабо типизирован и неявно преобразуется в / из int (или любого типа хранилища, который вы выбираете), он чувствует себя менее странно, если значения не совпадают с перечисленными константами.

Недостатком является то, что это описывается как «переходный» ...

NB. этот вариант добавляет свои перечисляемые константы как во вложенную, так и во вложенную область, но вы можете обойти это с помощью пространства имен:

namespace E5 {
    enum Enum : int { A, B, C };
}
E5::Enum x = E5::A; // or E5::Enum::A
Бесполезный
источник
1
Другим недостатком слабо типизированных перечислений является то, что их константы загрязняют мое пространство имен, поскольку им не нужно добавлять префикс к имени перечисления. И это также может вызвать все виды странного поведения, если у вас есть два разных перечисления, оба с одним и тем же именем.
Даниэль А.А. Пельсмакер
Это правда. Вариант со слабым типом и указанным типом хранилища добавляет свои константы как в область видимости, так и в область видимости iiuc.
бесполезно
Перечислитель с незаданной областью объявляется только в окружающей области. Возможность определить его по enum-name является частью правил поиска, а не объявления. C ++ 11 7.2 / 10: Каждое имя-перечисление и каждый перечислитель с незаданной областью объявляются в области, которая непосредственно содержит спецификатор-перечисления. Каждый перечислитель с областью действия объявляется в области перечисления. Эти имена подчиняются правилам области действия, определенным для всех имен в (3.3) и (3.4).
Ларс Виклунд
1
в C ++ 11 у нас есть std :: under_type, который предоставляет базовый тип перечисления. Итак, у нас есть 'template <typename IntegralType> struct Integral {typedef typename std :: under_type <IntegralType> :: type Type; }; `В C ++ 14 это еще проще упростить 'шаблон <typename IntegralType> struct Integral {typedef std :: under_type_t <IntegralType> Type; };
emsr
4

Вы можете определить безопасные для типов флаги перечисления в C ++ 11, используя std::enable_if. Это элементарная реализация, в которой могут отсутствовать некоторые вещи:

template<typename Enum, bool IsEnum = std::is_enum<Enum>::value>
class bitflag;

template<typename Enum>
class bitflag<Enum, true>
{
public:
  constexpr const static int number_of_bits = std::numeric_limits<typename std::underlying_type<Enum>::type>::digits;

  constexpr bitflag() = default;
  constexpr bitflag(Enum value) : bits(1 << static_cast<std::size_t>(value)) {}
  constexpr bitflag(const bitflag& other) : bits(other.bits) {}

  constexpr bitflag operator|(Enum value) const { bitflag result = *this; result.bits |= 1 << static_cast<std::size_t>(value); return result; }
  constexpr bitflag operator&(Enum value) const { bitflag result = *this; result.bits &= 1 << static_cast<std::size_t>(value); return result; }
  constexpr bitflag operator^(Enum value) const { bitflag result = *this; result.bits ^= 1 << static_cast<std::size_t>(value); return result; }
  constexpr bitflag operator~() const { bitflag result = *this; result.bits.flip(); return result; }

  constexpr bitflag& operator|=(Enum value) { bits |= 1 << static_cast<std::size_t>(value); return *this; }
  constexpr bitflag& operator&=(Enum value) { bits &= 1 << static_cast<std::size_t>(value); return *this; }
  constexpr bitflag& operator^=(Enum value) { bits ^= 1 << static_cast<std::size_t>(value); return *this; }

  constexpr bool any() const { return bits.any(); }
  constexpr bool all() const { return bits.all(); }
  constexpr bool none() const { return bits.none(); }
  constexpr operator bool() { return any(); }

  constexpr bool test(Enum value) const { return bits.test(1 << static_cast<std::size_t>(value)); }
  constexpr void set(Enum value) { bits.set(1 << static_cast<std::size_t>(value)); }
  constexpr void unset(Enum value) { bits.reset(1 << static_cast<std::size_t>(value)); }

private:
  std::bitset<number_of_bits> bits;
};

template<typename Enum>
constexpr typename std::enable_if<std::is_enum<Enum>::value, bitflag<Enum>>::type operator|(Enum left, Enum right)
{
  return bitflag<Enum>(left) | right;
}
template<typename Enum>
constexpr typename std::enable_if<std::is_enum<Enum>::value, bitflag<Enum>>::type operator&(Enum left, Enum right)
{
  return bitflag<Enum>(left) & right;
}
template<typename Enum>
constexpr typename std::enable_if_t<std::is_enum<Enum>::value, bitflag<Enum>>::type operator^(Enum left, Enum right)
{
  return bitflag<Enum>(left) ^ right;
}

Обратите внимание number_of_bits, что компилятор, к сожалению, не может быть заполнен, так как C ++ не имеет никакого способа сделать интроспективный анализ возможных значений перечисления.

Редактировать: На самом деле я исправлен, возможно получить компилятор number_of_bitsдля вас.

Обратите внимание, что это может обрабатывать (крайне неэффективно) непостоянный диапазон значений перечисления. Давайте просто скажем, что не стоит использовать вышеприведенное с перечислением, как это, иначе начнется безумие:

enum class wild_range { start = 0, end = 999999999 };

Но, учитывая все обстоятельства, в конце концов, это вполне пригодное решение. Не нуждается в каких-либо поточках на стороне пользователя, безопасен по типу и в пределах своих возможностей, настолько эффективен, насколько это возможно (я сильно полагаюсь на std::bitsetкачество реализации здесь ;)).

rubenvb
источник
Я уверен, что пропустил некоторые перегрузки операторов.
rubenvb
2

я ненавидеть ненавижу макросы в моем C ++ 14 столько же, сколько следующий парень, но я привык использовать это повсеместно, и довольно либерально тоже:

#define ENUM_FLAG_OPERATOR(T,X) inline T operator X (T lhs, T rhs) { return (T) (static_cast<std::underlying_type_t <T>>(lhs) X static_cast<std::underlying_type_t <T>>(rhs)); } 
#define ENUM_FLAGS(T) \
enum class T; \
inline T operator ~ (T t) { return (T) (~static_cast<std::underlying_type_t <T>>(t)); } \
ENUM_FLAG_OPERATOR(T,|) \
ENUM_FLAG_OPERATOR(T,^) \
ENUM_FLAG_OPERATOR(T,&) \
enum class T

Сделать так просто, как

ENUM_FLAGS(Fish)
{
    OneFish,
    TwoFish,
    RedFish,
    BlueFish
};

И, как говорится, доказательство в пудинге:

ENUM_FLAGS(Hands)
{
    NoHands = 0,
    OneHand = 1 << 0,
    TwoHands = 1 << 1,
    LeftHand = 1 << 2,
    RightHand = 1 << 3
};

Hands hands = Hands::OneHand | Hands::TwoHands;
if ( ( (hands & ~Hands::OneHand) ^ (Hands::TwoHands) ) == Hands::NoHands)
{
    std::cout << "Look ma, no hands!" << std::endl;
}

Не стесняйтесь определять любой из отдельных операторов по своему усмотрению, но, по моему мнению, C / C ++ предназначен для взаимодействия с низкоуровневыми концепциями и потоками, и вы можете вырвать эти побитовые операторы из моих холодных мертвых рук. и я буду драться с вами всеми нечестивыми макросами и переворачивающими биты заклинаниями, которые я могу призвать, чтобы сохранить их.

Махмуд Аль-Кудси
источник
2
Если вы так сильно ненавидите макросы, почему бы не использовать правильную конструкцию C ++ и написать некоторые операторы шаблонов вместо макросов? Возможно, шаблонный подход лучше, потому что вы можете использовать std::enable_ifс, std::is_enumчтобы ограничить ваши свободные перегрузки операторов только для работы с перечисляемыми типами. Я также добавил операторы сравнения (используя std::underlying_type) и логический оператор not для дальнейшего сокращения разрыва без потери строгой типизации. Единственное , что я не могу соответствовать это неявное преобразование к BOOL, но flags != 0и !flagsдостаточно для меня.
monkey0506
1

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

Таким образом, вы должны иметь (с помощью оператора Bitshift для установки значений, например, 1 << 2 - то же самое, что двоичный код 100)

#define ENUM_1 1
#define ENUM_2 1 << 1
#define ENUM_3 1 << 2

и т.д

В C ++ у вас есть больше опций, определите новый тип, а не int (используйте typedef ) и установите значения, аналогичные приведенным выше; или определить битовое поле или вектор bools . Последние 2 очень компактны и имеют больше смысла для работы с флагами. Преимущество битового поля состоит в том, что он дает вам проверку типов (и, следовательно, intellisense).

Я бы сказал (очевидно, субъективно), что программист C ++ должен использовать битовое поле для вашей проблемы, но я склонен видеть подход #define, часто используемый программами C в программах C ++.

Я полагаю, что битовое поле наиболее близко к перечислению C #, поэтому C # попытался перегрузить перечисление, чтобы быть типом битового поля, странно - перечисление действительно должно быть типом "одиночный выбор".

gbjbaanb
источник
11
использование макросов в с ++ таким способом - плохо
BЈовић
3
C ++ 14 позволяет вам определять двоичные литералы (например 0b0100), так что 1 << nформат устарел.
Роб К
Может быть, вы имели в виду битсет вместо битфилд.
Хорхе Беллон
1

Краткий пример перечисляемых ниже флагов очень похож на C #.

О подходе, на мой взгляд: меньше кода, меньше ошибок, лучше код.

#indlude "enum_flags.h"

ENUM_FLAGS(foo_t)
enum class foo_t
    {
     none           = 0x00
    ,a              = 0x01
    ,b              = 0x02
    };

ENUM_FLAGS(foo2_t)
enum class foo2_t
    {
     none           = 0x00
    ,d              = 0x01
    ,e              = 0x02
    };  

int _tmain(int argc, _TCHAR* argv[])
    {
    if(flags(foo_t::a & foo_t::b)) {};
    // if(flags(foo2_t::d & foo_t::b)) {};  // Type safety test - won't compile if uncomment
    };

ENUM_FLAGS (T) - это макрос, определенный в enum_flags.h (менее 100 строк, бесплатный для использования без ограничений).

Юрий Ярышев
источник
1
файл enum_flags.h такой же, как в первой ревизии вашего вопроса? если да, вы можете использовать ревизионный URL для ссылки на него: http://programmers.stackexchange.com/revisions/205567/1
gnat
+1 выглядит хорошо, чисто. Я попробую это в нашем проекте SDK.
Гарет Клаборн
1
@GaretClaborn Это то, что я бы назвал чистым: paste.ubuntu.com/23883996
17
1
Конечно, пропустил ::typeтам. Исправлено: paste.ubuntu.com/23884820
30
@sehe эй, код шаблона не должен быть разборчивым и иметь смысл. что это за колдовство? хорошо .... этот фрагмент открыт для использования LOL
Гарет Клаборн
0

Есть еще один способ снять кожу с кошки:

Вместо того, чтобы перегружать битовые операторы, по крайней мере некоторые могут предпочесть просто добавить 4 строки, чтобы помочь вам обойти это неприятное ограничение перечислений:

#include <cstdio>
#include <cstdint>
#include <type_traits>

enum class Foo : uint16_t { A = 0, B = 1, C = 2 };

// ut_cast() casts the enum to its underlying type.
template <typename T>
inline auto ut_cast(T x) -> std::enable_if_t<std::is_enum_v<T>,std::underlying_type_t<T>>
{
    return static_cast<std::underlying_type_t<T> >(x);
}

int main(int argc, const char*argv[])
{
   Foo foo{static_cast<Foo>(ut_cast(Foo::B) | ut_cast(Foo::C))};
   Foo x{ Foo::C };
   if(0 != (ut_cast(x) & ut_cast(foo)) )
       puts("works!");
    else 
        puts("DID NOT WORK - ARGHH");
   return 0;
}

Конечно, вы должны печатать эту ut_cast()вещь каждый раз, но, с другой стороны, это дает более читаемый код в том же смысле, что и при использовании static_cast<>(), по сравнению с неявным преобразованием типов или operator uint16_t()другими вещами.

И давайте будем честными, использование типа, Fooкак в приведенном выше коде, имеет свои опасности:

Где-то еще кто-то может переключить регистр на переменную fooи не ожидать, что он содержит более одного значения ...

Так что засорение кода ut_cast()помогает предупредить читателей, что происходит что-то подозрительное.

BitTickler
источник