Введите безопасные флаги enum bit
Я хочу использовать набор битовых флагов для моей текущей проблемы. Эти флаги (красиво) определены как часть 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
содержащие значения, не определенные в перечислении, вызывают неопределенное поведение? Есть ли какие-то предостережения, о которых нужно знать?
5 ответов
Эта перегрузка действительно обеспечивает безопасность типов?
В этом случае да. Допустимый диапазон значений для перечисления идет по крайней мере до (но не обязательно включая) следующей по величине степени двух после самого большого именованного перечислителя, чтобы позволить его использовать для подобных битовых масок. Таким образом, любая побитовая операция с двумя значениями даст значение, представляемое этим типом.
Вызывает ли приведение типа int, содержащего значения, не определенные в enum, неопределенное поведение?
Нет, пока значения представляются перечислением, которое они здесь.
Есть ли какие-то предостережения, о которых нужно знать?
Если бы вы выполняли такие операции, как арифметика, которая могла бы вывести значение из диапазона, вы бы получили результат, определенный реализацией, но не неопределенное поведение.
Если вы думаете о безопасности типов, лучше использовать 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);
Значения ваших констант не закрываются в OR. Другими словами, возможно, что результат ИЛИ двух констант ENUM приведет к значению, которое не является константой ENUM:
0x30 == FIVE | SIX;
Стандарт говорит, что это нормально, перечисление может иметь значение, не равное ни одному из перечислителей (констант). Предположительно, это разрешить этот тип использования.
На мой взгляд, это небезопасно, потому что если вы посмотрите на реализацию enumTest
Вы должны знать, что тип аргумента ENUM
но это может иметь значение, которое не является ENUM
переписчик.
Я думаю, что если это просто битовые флаги, делайте то, что от вас хочет компилятор: используйте int
за комбинацию флагов.
С простым enum
такой как твой:
enum ENUM
{
ONE = 0x01,
TWO = 0x02,
...
};
это определяется реализацией того, что является базовым типом (скорее всего int
) 1, но пока вы собираетесь использовать |
(побитовое или) для создания масок, результат никогда не потребует более широкого типа, чем наибольшее значение из этого перечисления.
[1] "Базовый тип перечисления является целочисленным типом, который может представлять все значения перечислителя, определенные в перечислении. Это определяется реализацией, какой интегральный тип используется в качестве базового типа для перечисления, за исключением того, что базовый тип не должен быть больше чем int
если значение перечислителя не может вписаться в int
или же unsigned int
".
Это мой подход к битовым флагам:
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
Вы можете подумать, что вы хотите или ОДИН или ДВА, не обязательно оба. Вот почему я предпочитаю использовать +
сложить флаги и ==
проверить, установлен ли флаг.
Смотрите этот пост в блоге для более подробной информации о моей предложенной реализации.