Введите безопасные флаги бит enum

Я хочу использовать набор битовых флагов для моей текущей проблемы. Эти флаги (красиво) определены как часть enum, однако я понимаю, что когда вы OR два значения из перечисления, тип возврата операции OR имеет тип int.

То, что я сейчас ищу, - это решение, которое позволит пользователям битной маски оставаться безопасным по типу, поэтому я создал следующую перегрузку для operator |

enum ENUM
{
    ONE     = 0x01,
    TWO     = 0x02,
    THREE   = 0x04,
    FOUR    = 0x08,
    FIVE    = 0x10,
    SIX     = 0x20
};

ENUM operator | ( ENUM lhs, ENUM rhs )
{
    // Cast to int first otherwise we'll just end up recursing
    return static_cast< ENUM >( static_cast< int >( lhs ) | static_cast< int >( rhs ) );
}

void enumTest( ENUM v )
{
}

int main( int argc, char **argv )
{
    // Valid calls to enumTest
    enumTest( ONE | TWO | FIVE );
    enumTest( TWO | THREE | FOUR | FIVE );
    enumTest( ONE | TWO | THREE | FOUR | FIVE | SIX );

    return 0;
}

Действительно ли эта перегрузка обеспечивает безопасность типов? Выбрасывает ли int значения, не определенные в перечислении, вызывает поведение undefined? Есть ли какие-либо оговорки, о которых нужно знать?

Ответ 1

Действительно ли эта перегрузка обеспечивает безопасность типа?

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

Выполняет ли листинг int, содержащее значения, не определенные в перечислении, вызывает поведение undefined?

Нет, до тех пор, пока значения будут представлены через перечисление, которое они здесь.

Есть ли какие-либо оговорки, о которых нужно знать?

Если вы выполняли такие операции, как арифметика, которая могла бы вывести значение за пределы диапазона, тогда вы получите результат, определенный реализацией, но не undefined behavoiur.

Ответ 2

Если вы думаете о безопасности типов, лучше использовать std:: bitset

enum BITS { A, B, C, D }; 
std::bitset<4> bset, bset1;
bset.set(A); bset.set(C);
bset1[B] = 1;
assert(bset[A] == bset[C]);
assert(bset[A] != bset[B]);
assert(bset1 != bset);

Ответ 3

Значения ваших констант не закрываются под символом ИЛИ. Другими словами, возможно, что результат OR двух констант ENUM приведет к значению, которое не является константой ENUM:

0x30 == FIVE | SIX;

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

По-моему, это не безопасно, потому что, если вы посмотрите на реализацию enumTest, вам нужно знать, что тип аргумента ENUM, но может иметь значение, которое не является перечислением ENUM.

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

Ответ 4

С помощью простого enum, такого как ваш:

enum ENUM
{
    ONE     = 0x01,
    TWO     = 0x02,
    ...
};

определяется реализацией типа базового типа (скорее всего int) 1 но пока вы собираетесь использовать | (побитовое) для создания масок, результат никогда не потребует более широкого типа, чем наибольшее значение из этого перечисления.


[1] "Основной тип перечисления является интегральным типом, который может представлять все значения перечисления, определенные в перечислении. определяется реализацией, который используется интегральным типом как базовый тип для перечисления, за исключением того, что базовый тип не должен превышать int, если значение перечислителя не может быть помещено в int или unsigned int."

Ответ 5

Это мой подход к битовым флагам:

template<typename E>
class Options {
      unsigned long values;
      constexpr Options(unsigned long v, int) : values{v} {}
   public:
      constexpr Options() : values(0) {}
      constexpr Options(unsigned n) : values{1UL << n} {}
      constexpr bool operator==(Options const& other) const {
         return (values & other.values) == other.values;
      }
      constexpr bool operator!=(Options const& other) const {
         return !operator==(other);
      }
      constexpr Options operator+(Options const& other) const {
         return {values | other.values, 0};
      }
      Options& operator+=(Options const& other) {
         values |= other.values;
         return *this;
      }
      Options& operator-=(Options const& other) {
         values &= ~other.values;
         return *this;
      }
};

#define DECLARE_OPTIONS(name) class name##__Tag; using name = Options
#define DEFINE_OPTION(name, option, index) constexpr name option(index)

Вы можете использовать его так:

DECLARE_OPTIONS(ENUM);
DEFINE_OPTIONS(ENUM, ONE, 0);
DEFINE_OPTIONS(ENUM, TWO, 1);
DEFINE_OPTIONS(ENUM, THREE, 2);
DEFINE_OPTIONS(ENUM, FOUR, 3);

Тогда ONE + TWO по-прежнему имеет тип ENUM. И вы можете повторно использовать класс для определения нескольких наборов флагов бит, которые имеют разные несовместимые типы.

Мне лично не нравится использовать | и & для установки и тестирования бит. Это логическая операция, которая должна быть выполнена для установки и тестирования, но они не выражают смысл операции, если вы не думаете о побитовых операциях. Если вы читаете ONE | TWO, вы можете подумать, что хотите либо ОДНОГО, либо ДВА, не обязательно того и другого. Вот почему я предпочитаю использовать + для добавления флагов вместе и ==, чтобы проверить, установлен ли флаг.

Подробнее о моей предлагаемой реализации см. здесь: http://www.crisluengo.net/index.php/archives/851