Використання розширених перерахунків для бітових прапорів у C ++


60

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

У C # перерахунки використовуються не лише для зберігання окремих значень, але й для зберігання побітових комбінацій прапорів, згідно рекомендацій Microsoft . Такі перерахунки (як правило, але не обов'язково) прикрашені [Flags]атрибутом. Щоб полегшити життя розробників, побітні оператори (АБО, І т.д. тощо) перевантажуються, так що ви можете легко зробити щось подібне (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));

    Але це зменшить усе до рівня 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 # я схиляюся з підходом 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якщо не вказано інше, і, таким чином, завжди є фіксований базовий тип.])
Час Джастіна

Відповіді:


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 :: underlying_type_t є C ++ 14. Можна використовувати std :: underlying_type <T> ::: тип C ++ 11.
ddevienne

14
Чому ви використовуєте static_cast<T>для введення, але тут виклик стилю C?
Руслан

2
@Ruslan Я другий це питання
audiFanatic

Чому ви навіть турбуєтеся з std :: underlying_type_t, коли вже знаєте, що це int?
poizan42

1
Якщо SBJFrameDragвизначено в класі, а |пізніше -оператор буде використаний у визначеннях того ж класу, як би ви визначили оператора таким, щоб його можна було використовувати в класі?
HelloGoodbye

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
Ще одним недоліком слабко набраних переписів є те, що їхні константи забруднюють мій простір імен, оскільки їм не потрібно вказувати прізвище enum. І це також може викликати всілякі дивні поведінки, якщо у вас є два різні переписки, обидва з членом з тим же ім’ям.
Daniel AA Pelsmaeker

Це правда. Слабо типізований варіант із зазначеним типом зберігання додає свої константи як до області, що охоплює, так і до її власної області, iiuc.
Марно

Нескопаний перелік зазначається лише в навколишньому просторі. Можливість кваліфікувати його за допомогою enum-name є частиною правил пошуку, а не декларацією. C ++ 11 7.2 / 10: Кожне ім'я enum та кожен незаписаний перелік оголошується в області, яка негайно містить специфікатор enum. Кожен перелік обчислень оголошується у межах переліку. Ці назви підкоряються правилам області застосування, визначеним для всіх імен у (3.3) та (3.4).
Ларс Віклунд

1
з C ++ 11 у нас є std :: underlying_type, який забезпечує базовий тип enum. Отже, у нас є 'template <typename IntegralType> struct Integral {typedef typename std :: underlying_type <IntegralType> :: type Type; }; `У C ++ 14 це ще більш спрощено до'template <typename IntegralType> struct Integral {typedef std :: underlying_type_t <IntegralType> Тип; };
emsr

4

Ви можете визначити безпечні для типу прапори enum у 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

2

Я ненависть майте макроси в моєму С ++ 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) та логічного оператора, який не працює, щоб надалі усунути прогалину, не втрачаючи при цьому сильного набору тексту. Єдине , що я не можу відповідати це неявне перетворення до BOOL, але flags != 0і !flagsдосить для мене.
monkey0506

1

Зазвичай слід визначити набір цілих значень, що відповідають однорозрядним наборам двійкових чисел, а потім додати їх разом. Це так, як це роблять програмісти на C.

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

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

тощо

У C ++ у вас є більше опцій, визначте новий тип, а не int (використовуйте typedef ) і аналогічно встановіть значення, як вище; або визначити бітове поле або вектор булів . Останні 2 дуже просторові та мають набагато більше сенсу для роботи з прапорами. Бітфілд має перевагу в наданні перевірки типу (і, отже, інтеліссенса).

Я б сказав (очевидно, суб'єктивно), що програміст на C ++ повинен використовувати бітфілд для вашої проблеми, але я схильний бачити підхід #define, який багато в програмах C використовується в програмах C ++.

Я вважаю, що бітфілд є найближчим до переліку C #, чому C # намагався перевантажити перерахунок, щоб бути типом бітфілда, дивно - перерахунок дійсно повинен бути типом "один вибір".


11
використовувати макроси в c ++ таким чином погано
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/reitions/205567/1
gnat

+1 виглядає добре, чисто. Я спробую це в нашому проекті SDK.
Гарет Клаборн

1
@GaretClaborn Ось що я б назвав чистим: paste.ubuntu.com/23883996
sehe

1
Звичайно, ::typeтам пропустили . Виправлено: paste.ubuntu.com/23884820
1717

@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()допомагає сповістити читачів про те, що відбувається щось рибне.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.