C ++에서 비트 플래그에 범위가 지정된 열거 형 사용


60

enum X : int(C 번호) 또는 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 #에서와 똑같은 방식으로 C ++ 11 열거 형을 사용하려고합니다. C ++ 11에서는 범위가 지정된 열거 형의 비트 연산자가 오버로드되지 않으므로 오버 로드하고 싶었습니다 .

이것은 논쟁을 불러 일으켰으며, 의견은 세 가지 옵션 사이에서 다양하게 보인다.

  1. 열거 형의 변수는 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 ++에서 가장 널리 인식되는 접근법은 무엇입니까? 비트 필드와 처리 할 때 당신은 어떤 방법을 사용합니까 ?

물론 세 가지 접근 방식이 모두 효과가 있기 때문에 저는 개인적으로 선호되는 것이 아니라 사실적이고 기술적 인 이유, 일반적으로 받아 들여지는 규칙을 찾고 있습니다.

예를 들어, C # 배경으로 인해 C ++에서 접근법 1을 사용하는 경향이 있습니다. 이것은 내 개발 환경이 가능한 값을 암시 할 수 있다는 추가 이점을 가지고 있으며 과부하 된 열거 형 연산자를 사용하면 작성하고 이해하기 쉽고 깨끗합니다. 그리고 메소드 서명은 어떤 종류의 가치를 기대하는지 명확하게 보여줍니다. 그러나 여기에있는 대부분의 사람들 은 나에게 동의하지 않을 것입니다. 아마 그럴만한 이유가 있습니다.


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_tC ++ 14 기능입니다.)


6
std :: underlying_type_t는 C ++ 14입니다. C ++ 11에서 std :: underlying_type <T> :: type을 사용할 수 있습니다.
ddevienne

14
static_cast<T>입력에 사용하고 있지만 결과를 위해 C 스타일 캐스트를 사용합니까?
Ruslan

2
@Ruslan이 질문을 두 번째
audiFanatic

왜 이미 int임을 알 때 std :: underlying_type_t를 귀찮게합니까?
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
약한 유형의 열거 형의 또 다른 단점은 상수가 열거 형 이름을 접두사로 사용할 필요가 없기 때문에 상수가 내 네임 스페이스를 오염 시킨다는 것입니다. 그리고 같은 이름의 멤버가있는 두 개의 다른 열거 형이있는 경우 모든 종류의 이상한 동작이 발생할 수 있습니다.
Daniel AA Pelsmaeker

사실입니다. 지정된 스토리지 유형을 가진 약한 유형의 변형은 상수를 둘러싸는 범위 자체 범위 iiuc 에 모두 추가합니다 .
쓸모없는

범위가 지정되지 않은 열거자는 주변 범위에서만 선언됩니다. 열거 형 이름으로 자격을 부여하는 것은 선언이 아니라 조회 규칙의 일부입니다. C ++ 11 7.2 / 10 : 각 열거 형 이름과 범위가 지정되지 않은 열거자는 열거 형 지정자를 즉시 ​​포함하는 범위에서 선언됩니다. 각 범위 열거자는 열거 범위에서 선언됩니다. 이 이름은 (3.3) 및 (3.4)의 모든 이름에 대해 정의 된 범위 규칙을 따릅니다.
Lars Viklund

1
C ++ 11을 사용하면 기본 유형의 열거 형을 제공하는 std :: underlying_type이 있습니다. 그래서 우리는 'template <typename IntegralType> struct Integral {typedef typename std :: underlying_type <IntegralType> :: type Type; }; `C ++ 14에서는 <typename IntegralType> struct Integral {typedef std :: underlying_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_bitsC ++이 열거의 가능한 값을 성찰 할 수있는 방법을 가지고 있지 않기 때문에 불행하게도, 컴파일러에 의해 작성 될 수 없습니다.

편집 : 실제로 나는 정정되었습니다 number_of_bits. 컴파일러를 채울 수 있습니다.

이것은 비 연속적인 열거 형 값 범위를 처리 할 수 ​​있다는 점에 유의하십시오. 위와 같은 열거 형과 함께 위의 내용을 사용하는 것이 좋지 않다고 가정 해 봅시다.

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

그러나이 모든 것이 결국에는 꽤 유용한 해결책이라고 생각합니다. 사용자 측 bitfiddling이 필요하지 않으며 유형 안전하고 범위 내에서 가능한 한 효율적입니다 ( std::bitset구현 품질에 크게 의존 하고 있습니다 ;)).


나는 연산자의 과부하를 놓쳤다 고 확신합니다.
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강력한 입력을 잃지 않고 격차를 해소하기 위해 비교 연산자 (을 사용하여 )와 논리 연산자를 추가했습니다. 내가 일치하지 않을 수있는 유일한 방법은 부울에 암시 적 변환이지만, 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 사용 ) 대신 새 유형을 정의하고 위와 같이 값을 설정하십시오. 또는 비트 필드 또는 bools 벡터를 정의하십시오 . 마지막 2 개는 공간 효율성이 뛰어나며 플래그 처리에 훨씬 적합합니다. 비트 필드는 유형 검사 (따라서 지능적)를 제공하는 이점이 있습니다.

C ++ 프로그래머가 문제에 비트 필드를 사용해야한다고 말하지만 분명히 C ++ 프로그램에서 C 프로그램이 많이 사용하는 #define 접근법을 보는 경향이 있습니다.

비트 필드가 C #의 열거 형에 가장 가까운 것으로 가정합니다. C #에서 열거 형을 비트 필드 형식으로 오버로드하려고 시도한 이유는 이상합니다. 열거 형은 실제로 "단일 선택"형식이어야합니다.


11
이런 식으로 C ++에서 매크로를 사용하는 것은 나쁘다
BЈовић

3
C ++ 14에서는 바이너리 리터럴 (예 :)을 정의 할 수 0b0100있으므로 1 << n형식이 더 이상 사용되지 않습니다.
Rob K

어쩌면 비트 필드 대신 비트 세트 를 의미 했을 수도 있습니다 .
Jorge Bellon

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 프로젝트에서 시도해 보겠습니다.
Garet Claborn

1
@GaretClaborn 이것은 내가 깨끗하게 부르는 것입니다 : paste.ubuntu.com/23883996
sehe

1
물론, ::type거기를 놓쳤다 . 고정 : paste.ubuntu.com/23884820
sehe

@sehe, 템플릿 코드는 읽기 쉽고 이해하기 쉽지 않습니다. 이 마법은 무엇입니까? 좋은 ....이 조각은 롤 사용하는 오픈
Garet Claborn

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위 코드에서와 같이 type 을 사용 하면 위험이 있습니다.

다른 곳에서 누군가 변수를 바꾸는 경우가 foo있고 둘 이상의 값을 가질 것으로 기대하지 않을 수도 있습니다 ...

따라서 코드를 흩 뜨리면 ut_cast()독자에게 비린내가 일어나고 있음을 경고 하는 데 도움이됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.