В поисках лучшей битфлагской переписи

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

У нас есть enum который отбрасывает значения своих членов во вмещающую область, но неявно преобразует их в их базовый тип, поэтому может использоваться как если бы они были битовыми флагами, но отказывались переназначаться обратно в enum без приведения.

У нас есть enum class который решает проблему области имен, так что их значения должны быть явно названы MyEnum::MyFlag или даже MyClass::MyEnum::MyFlag, но они неявно преобразуются в свой базовый тип, поэтому не могут использоваться как битовые флаги без бесконечного литья взад и вперед.

И, наконец, у нас есть старые битовые поля из C такие как:

struct FileFlags {
   unsigned ReadOnly : 1;
   unsigned Hidden : 1;
   ...
};

Недостатком которого является отсутствие хорошего способа инициализации себя как целого - нужно прибегнуть к использованию memset или приведению адреса или тому подобного, чтобы перезаписать все значение или инициализировать его все сразу или иным образом манипулировать несколькими битами одновременно. Он также страдает от невозможности назвать значение данного флага, в отличие от его адреса - поэтому нет имени, представляющего 0x02, тогда как при использовании перечислений такое имя существует, поэтому с помощью перечислений легко назвать комбинацию из флаги, такие как FileFlags::ReadOnly | FileFlags::Hidden FileFlags::ReadOnly | FileFlags::Hidden - просто нет хорошего способа сказать так много для битовых полей.

Кроме того, у нас все еще есть простой constexpr или #define для именования битовых значений, а затем мы просто не используем перечисления вообще. Это работает, но полностью отделяет битовые значения от базового типа битового флага. Возможно, это в конечном итоге не самый плохой подход, особенно если значения битовых флагов являются constexpr внутри структуры, чтобы дать им собственную область имен?

struct FileFlags {
    constexpr static uint16_t ReadOnly = 0x01u;
    constexpr static uint16_t Hidden = 0x02u;
    ...
}

Таким образом, в настоящее время у нас есть много методик, ни один из которых не может дать действительно убедительный способ сказать,

Вот тип, который имеет следующие допустимые битовые флаги, имеет свою собственную область имен, и эти биты и тип должны свободно использоваться со стандартными побитовыми операторами, такими как | & ^ ~, и они должны быть сопоставимы с целочисленными значениями, такими как 0, а результат любых побитовых операторов должен оставаться именованным типом, а не переходить в интеграл

Все это говорит о том, что существует множество попыток создать вышеуказанную сущность в C++ -

  1. Команда Windows OS разработала простой макрос, который генерирует код C++, чтобы определить необходимые недостающие операторы для данного типа перечисления DEFINE_ENUM_FLAG_OPERATORS(EnumType) который затем определяет operator | & ^ ~ и связанные операции назначения, такие как | = и т.д.
  2. "grisumbras" есть проект общественного GIT для включения bitflag семантики с контекстными перечислениями здесь, который использует enable_if мету программирования, чтобы данное перечисление преобразовать в тип bitflag, который поддерживает недостающие операторы и снова тихо.
  3. Не зная вышеизложенного, я написал относительно простую оболочку bit_flags, которая определяет для себя все побитовые операторы, так что можно использовать bit_flags<EnumType> flags а затем flags имеет битовую семантику. Чего это не может сделать, так это позволить перечисляемой базе фактически правильно обрабатывать побитовые операторы напрямую, поэтому вы не можете сказать EnumType::ReadOnly | EnumType::Hidden EnumType::ReadOnly | EnumType::Hidden даже при использовании bit_flags<EnumType> потому что само базовое перечисление все еще не поддерживает необходимые операторы. Я должен был в конечном итоге сделать то же самое, что и # 1 и # 2 выше, и включить operator | (EnumType, EnumType) operator | (EnumType, EnumType) для различных побитовых операторов, требуя от пользователей объявить специализацию для template <> struct is_bitflag_enum<EnumType>: std::true_type {}; для их перечисления, такого как template <> struct is_bitflag_enum<EnumType>: std::true_type {};

В конечном счете, проблема с № 1, № 2 и № 3 заключается в том, что невозможно (насколько я знаю) определить отсутствующие операторы в самом перечислении (как в № 1) или определить необходимый тип активатора ( например, template <> struct is_bitflag_enum<EnumType>: std::true_type {}; как в # 2 и частично # 3) в области видимости класса. Это должно происходить вне класса или структуры, так как C++ просто не имеет механизма, о котором я знаю, который позволил бы мне делать такие объявления внутри класса.

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

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

Все это все еще оставляет у меня досадное чувство, что это может быть намного лучше, чем есть!

Конечно, должен быть - и, возможно, есть, но я еще не понял, - подход к перечислениям, чтобы разрешить для них побитовые операторы, в то же время позволяя им объявляться и использоваться в рамках включающего класса...

У кого-нибудь есть вайп или подход, который я не рассматривал выше, который позволил бы мне "лучший из всех возможных миров" в этом?

Ответы

Ответ 1

Например

// union only for convenient bit access. 
typedef union a
{ // it has its own name-scope
    struct b
     {
         unsigned b0 : 1;
         unsigned b2 : 1;
         unsigned b3 : 1;
         unsigned b4 : 1;
         unsigned b5 : 1;
         unsigned b6 : 1;
         unsigned b7 : 1;
         unsigned b8 : 1;
         //...
     } bits;
    unsigned u_bits;
    // has the following valid bit-flags in it
    typedef enum {
        Empty = 0u,
        ReadOnly = 0x01u,
        Hidden  = 0x02u
    } Values;
    Values operator =(Values _v) { u_bits = _v; return _v; }
     // should be freely usable with standard bitwise operators such as | & ^ ~   
    union a& operator |( Values _v) { u_bits |= _v; return *this; }
    union a& operator &( Values _v) { u_bits &= _v; return *this; }
    union a& operator |=( Values _v) { u_bits |= _v; return *this; }
    union a& operator &=( Values _v) { u_bits &= _v; return *this; }
     // ....
    // they should be comparable to integral values such as 0
    bool operator <( unsigned _v) { return u_bits < _v; }
    bool operator >( unsigned _v) { return u_bits > _v; }
    bool operator ==( unsigned _v) { return u_bits == _v; }
    bool operator !=( unsigned _v) { return u_bits != _v; }
} BITS;


int main()
 {
     BITS bits;
     int integral = 0;

     bits = bits.Empty;

     // they should be comparable to integral values such as 0
     if ( bits == 0)
     {
         bits = bits.Hidden;
         // should be freely usable with standard bitwise operators such as | & ^ ~
         bits = bits | bits.ReadOnly;
         bits |= bits.Hidden;
         // the result of any bitwise operators should remain the named type, and not devolve into an integral
         //bits = integral & bits; // error
         //bits |= integral; // error
     }
 }

Ответ 2

Я использую enum class со следующими шаблонными операторами:

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM operator |( ENUM lhs, ENUM rhs )
{
    return static_cast< ENUM >( static_cast< UInt32 >( lhs ) | static_cast< UInt32 >( rhs ));
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator |=( ENUM& lhs, ENUM rhs )
{
    lhs = lhs | rhs;
    return lhs;
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline UInt32 operator &( ENUM lhs, ENUM rhs )
{
    return static_cast< UInt32 >( lhs ) & static_cast< UInt32 >( rhs );
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator &=( ENUM& lhs, ENUM rhs )
{
    lhs = lhs & rhs;
    return lhs;
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator &=( ENUM& lhs, int rhs )
{
    lhs = static_cast< ENUM >( static_cast< int >( lhs ) & rhs );
    return lhs;
}

Если вас беспокоят вышеперечисленные операторы, просачивающиеся в другие перечисления, я думаю, вы можете инкапсулировать их в том же пространстве имен, где объявлено перечисление, или даже просто реализовать их на основе перечисления (я использовал макрос для тот). Вообще говоря, я считал это излишним, и теперь я объявил их в моем пространстве имен верхнего уровня для использования любым кодом.

Ответ 3

Я использую подход Xaqq FlagSet на Code Review SE.

Ключ заключается в том, чтобы ввести новый тип, который будет служить "контейнером" для одного или нескольких включенных значений из фиксированного списка опций. Упомянутый контейнер представляет собой обертку вокруг bitset которая принимает в качестве входных данных экземпляры перечисления области видимости.

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

Для производства я внес некоторые изменения в связанный код; некоторые из них обсуждаются в комментариях на странице Code Review.

Ответ 4

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

Например, чтобы избежать специализации is_bitflag_enum, специализируйте структуру, которая содержит перечисления и операторы. Это как # 2, и все еще не может быть сделано в классе.

#include <type_traits>

template<class Tag>
struct bitflag {
    enum class type;

#define DEFINE_BITFLAG_OPERATOR(OP) \
    friend constexpr type operator OP(type lhs, type rhs) noexcept { \
        typedef typename ::std::underlying_type<type>::type underlying; \
        return static_cast<type>(static_cast<underlying>(lhs) OP static_cast<underlying>(rhs)); \
    } \
    friend constexpr type& operator OP ## = (type& lhs, type rhs) noexcept { \
        return (lhs = lhs OP rhs); \
    }

    DEFINE_BITFLAG_OPERATOR(|)
    DEFINE_BITFLAG_OPERATOR(&)
    DEFINE_BITFLAG_OPERATOR(^)

#undef DEFINE_BITFLAG_OPERATOR

#define DEFINE_BITFLAG_OPERATOR(OP) \
    friend constexpr bool operator OP(type lhs, typename ::std::underlying_type<type>::type rhs) noexcept { \
        return static_cast<typename ::std::underlying_type<type>::type>(lhs) OP rhs; \
    } \
    friend constexpr bool operator OP(typename ::std::underlying_type<type>::type lhs, type rhs) noexcept { \
        return lhs OP static_cast<typename ::std::underlying_type<type>::type>(rhs); \
    }

    DEFINE_BITFLAG_OPERATOR(==)
    DEFINE_BITFLAG_OPERATOR(!=)
    DEFINE_BITFLAG_OPERATOR(<)
    DEFINE_BITFLAG_OPERATOR(>)
    DEFINE_BITFLAG_OPERATOR(>=)
    DEFINE_BITFLAG_OPERATOR(<=)

#undef DEFINE_BITFLAG_OPERATOR

    friend constexpr type operator~(type e) noexcept {
        return static_cast<type>(~static_cast<typename ::std::underlying_type<type>::type>(e));
    }

    friend constexpr bool operator!(type e) noexcept {
        return static_cast<bool>(static_cast<typename ::std::underlying_type<type>::type>(e));
    }
};

// The 'struct file_flags_tag' (Which declares a new type) differentiates between different
// enum classes declared
template<> enum class bitflag<struct file_flags_tag>::type {
    none = 0,
    readable = 1 << 0,
    writable = 1 << 1,
    executable = 1 << 2,
    hidden = 1 << 3
};

using file_flags = bitflag<file_flags_tag>::type;

bool is_executable(file_flags f) {
    return (f & file_flags::executable) == 0;
}

Вы также можете сделать один макрос для определения каждой функции друга. Это похоже на # 1, но все это в рамках класса.

#include <type_traits>

#define MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(OP, ENUM_TYPE) \
    friend constexpr ENUM_TYPE operator OP(ENUM_TYPE lhs, ENUM_TYPE rhs) noexcept { \
        typedef typename ::std::underlying_type<ENUM_TYPE>::type underlying; \
        return static_cast<ENUM_TYPE>(static_cast<underlying>(lhs) OP static_cast<underlying>(rhs)); \
    } \
    friend constexpr ENUM_TYPE& operator OP ## = (ENUM_TYPE& lhs, ENUM_TYPE rhs) noexcept { \
        return (lhs = lhs OP rhs); \
    }

#define MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(OP, ENUM_TYPE) \
    friend constexpr bool operator OP(ENUM_TYPE lhs, typename ::std::underlying_type<ENUM_TYPE>::type rhs) noexcept { \
        return static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(lhs) OP rhs; \
    } \
    friend constexpr bool operator OP(typename ::std::underlying_type<ENUM_TYPE>::type lhs, ENUM_TYPE rhs) noexcept { \
        return lhs OP static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(rhs); \
    }


#define MAKE_BITFLAG_FRIEND_OPERATORS(ENUM_TYPE) \
    public: \
    MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(|, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(&, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(^, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(==, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(!=, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(<, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(>, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(>=, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(<=, ENUM_TYPE) \
    friend constexpr ENUM_TYPE operator~(ENUM_TYPE e) noexcept { \
        return static_cast<ENUM_TYPE>(~static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(e)); \
    } \
    friend constexpr bool operator!(ENUM_TYPE e) noexcept { \
        return static_cast<bool>(static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(e)); \
    }

// ^ The above in a header somewhere

class my_class {
public:
    enum class my_flags {
        none = 0, flag_a = 1 << 0, flag_b = 1 << 2
    };

    MAKE_BITFLAG_FRIEND_OPERATORS(my_flags)

    bool has_flag_a(my_flags f) {
        return (f & my_flags::flag_a) == 0;
    }
};

Ответ 5

Внедрение собственного битового набора с использованием выбора целочисленного типа не сложно. Проблема с перечислением состоит в том, что отсутствует необходимая метаинформация для адаптации к битову. Но все же с надлежащим мета-программированием и признаками включения флагов можно иметь такой синтаксис:

flagset<file_access_enum>   rw = bit(read_access_flag)|bit(write_access_flag);