Sử dụng enum phạm vi cho cờ bit trong C ++


60

Một enum X : int(C #) hoặc enum class X : int(C ++ 11) là một loại có trường bên trong ẩn intcó thể chứa bất kỳ giá trị nào. Ngoài ra, một số hằng số được xác định trước Xđược xác định trên enum. Có thể truyền enum đến giá trị nguyên của nó và ngược lại. Điều này hoàn toàn đúng trong cả C # và C ++ 11.

Trong C # enums không chỉ được sử dụng để giữ các giá trị riêng lẻ mà còn giữ các tổ hợp cờ theo bit, theo khuyến nghị của Microsoft . Các enum như vậy (thường, nhưng không nhất thiết) được trang trí với [Flags]thuộc tính. Để làm cho cuộc sống của các nhà phát triển dễ dàng hơn, các toán tử bitwise (OR, AND, v.v ...) bị quá tải để bạn có thể dễ dàng làm một cái gì đó như thế này (C #):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Tôi là một nhà phát triển C # có kinh nghiệm, nhưng tôi chỉ lập trình C ++ được vài ngày và tôi không biết các quy ước C ++. Tôi dự định sử dụng enum C ++ 11 theo cách chính xác giống như tôi đã từng làm trong C #. Trong C ++ 11, các toán tử bitwise trên enum có phạm vi không bị quá tải, vì vậy tôi muốn quá tải chúng .

Điều này đã thu hút một cuộc tranh luận và ý kiến ​​dường như khác nhau giữa ba lựa chọn:

  1. Một biến của loại enum được sử dụng để giữ trường bit, tương tự như 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)));
    

    Nhưng điều này sẽ chống lại triết lý enum được đánh máy mạnh mẽ của các enum có phạm vi của C ++ 11.

  2. Sử dụng một số nguyên đơn giản nếu bạn muốn lưu trữ một tổ hợp enum bitwise:

    void M(int flags);
    
    M(static_cast<int>(NumericType::Sign) | static_cast<int>(NumericType::ZeroPadding));
    

    Nhưng điều này sẽ giảm tất cả mọi thứ thành một int, khiến bạn không có manh mối nào về loại mà bạn phải đưa vào phương thức.

  3. Viết một lớp riêng biệt sẽ quá tải toán tử và giữ các cờ bitwise trong trường số nguyên ẩn:

    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);
    

    ( mã đầy đủ của người dùng315052 )

    Nhưng sau đó, bạn không có IntelliSense hoặc bất kỳ hỗ trợ nào để gợi ý cho bạn về các giá trị có thể.

Tôi biết đây là một câu hỏi chủ quan , nhưng: Tôi nên sử dụng phương pháp nào? Cách tiếp cận nào, nếu có, được công nhận rộng rãi nhất trong C ++? Cách tiếp cận nào bạn sử dụng khi xử lý các trường bit và tại sao ?

Tất nhiên vì cả ba phương pháp đều hoạt động, tôi đang tìm kiếm các lý do thực tế và kỹ thuật, các quy ước được chấp nhận chung, và không chỉ đơn giản là sở thích cá nhân.

Ví dụ, vì nền tảng C # của tôi, tôi có xu hướng đi theo cách tiếp cận 1 trong C ++. Điều này có thêm lợi ích là môi trường phát triển của tôi có thể gợi ý cho tôi về các giá trị có thể và với các toán tử enum quá tải, điều này dễ viết và dễ hiểu, và khá sạch sẽ. Và chữ ký phương thức cho thấy rõ loại giá trị mà nó mong đợi. Nhưng hầu hết mọi người ở đây không đồng ý với tôi, có lẽ vì lý do chính đáng.


2
Ủy ban ISO C ++ đã tìm thấy tùy chọn 1 đủ quan trọng để tuyên bố rõ ràng rằng phạm vi giá trị của enum bao gồm tất cả các tổ hợp cờ nhị phân. (Điều này có trước C ++ 03) Vì vậy, có một sự chấp thuận khách quan cho câu hỏi hơi chủ quan này.
MSalters

1
(Để làm rõ nhận xét của @MSalters, phạm vi của C ++ dựa trên loại cơ bản của nó (nếu là loại cố định) hoặc theo cách khác trên các điều tra viên của nó. Trong trường hợp sau, phạm vi này dựa trên bitfield nhỏ nhất có thể chứa tất cả các liệt kê được xác định ; ví dụ: đối enum E { A = 1, B = 2, C = 4, };với phạm vi là 0..7(3 bit). Do đó, tiêu chuẩn C ++ đảm bảo rõ ràng rằng # 1 sẽ luôn là một tùy chọn khả thi. [Cụ thể, enum classmặc định enum class : inttrừ khi có quy định khác và do đó luôn có loại cơ bản cố định.])
Thời gian Justin

Câu trả lời:


31

Cách đơn giản nhất là cung cấp cho người vận hành quá tải cho mình. Tôi đang nghĩ đến việc tạo ra một macro để mở rộng quá tải cơ bản cho mỗi loại.

#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;
}

(Lưu ý rằng đó type_traitslà một tiêu đề C ++ 11 và std::underlying_type_tlà một tính năng của C ++ 14.)


6
std :: ngầm_type_t là C ++ 14. Có thể sử dụng std :: ngầm_type <T> :: gõ trong C ++ 11.
ddevienne

14
Tại sao bạn sử dụng static_cast<T>cho đầu vào, nhưng kiểu C cho kết quả ở đây?
Ruslan

2
@Ruslan Tôi thứ hai câu hỏi này
audiFanatic

Tại sao bạn thậm chí bận tâm với std :: ngầm_type_t khi bạn đã biết nó là int?
poizan42

1
Nếu SBJFrameDragđược định nghĩa trong một lớp và trình điều khiển |sau đó được sử dụng trong các định nghĩa của cùng một lớp, làm thế nào bạn xác định toán tử sao cho nó có thể được sử dụng trong lớp?
HelloGoodbye

6

Trong lịch sử, tôi luôn luôn sử dụng phép liệt kê cũ (được đánh máy yếu) để đặt tên cho các hằng bit, và chỉ sử dụng lớp lưu trữ một cách rõ ràng để lưu trữ cờ kết quả. Ở đây, trách nhiệm sẽ thuộc về tôi để đảm bảo bảng liệt kê của tôi phù hợp với loại lưu trữ và để theo dõi mối liên hệ giữa trường và các hằng số liên quan.

Tôi thích ý tưởng về các enum được gõ mạnh, nhưng tôi không thực sự thoải mái với ý tưởng rằng các biến kiểu liệt kê có thể chứa các giá trị không nằm trong các hằng số liệt kê đó.

Ví dụ: giả sử bitwise hoặc đã bị quá tải:

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

Đối với tùy chọn thứ 3 của bạn, bạn cần một số mẫu soạn sẵn để trích xuất loại lưu trữ của bảng liệt kê. Giả sử chúng ta muốn buộc một loại cơ bản không dấu (chúng ta cũng có thể xử lý được ký, với một ít mã hơn):

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;

Điều này vẫn không cung cấp cho bạn IntelliSense hoặc tự động hoàn thành, nhưng việc phát hiện loại lưu trữ ít xấu hơn tôi dự kiến ​​ban đầu.


Bây giờ, tôi đã tìm thấy một giải pháp thay thế: bạn có thể chỉ định loại lưu trữ cho kiểu liệt kê được đánh máy yếu. Nó thậm chí có cú pháp giống như trong C #

enum E4 : int { ... };

Bởi vì nó được gõ yếu và chuyển đổi hoàn toàn thành / từ int (hoặc bất kỳ loại lưu trữ nào bạn chọn), nên cảm thấy ít lạ hơn khi có các giá trị không khớp với các hằng số liệt kê.

Nhược điểm là điều này được mô tả là "chuyển tiếp" ...

Lưu ý biến thể này thêm các hằng số liệt kê của nó vào cả phạm vi lồng nhau và phạm vi kèm theo, nhưng bạn có thể làm việc xung quanh nó với một không gian tên:

namespace E5 {
    enum Enum : int { A, B, C };
}
E5::Enum x = E5::A; // or E5::Enum::A

1
Một nhược điểm khác của các enum được đánh máy yếu là các hằng số của chúng làm ô nhiễm không gian tên của tôi, vì chúng không cần phải được thêm tiền tố với tên enum. Và điều đó cũng có thể gây ra tất cả các loại hành vi kỳ lạ nếu bạn có hai enum khác nhau với cả một thành viên có cùng tên.
Daniel AA Pelsmaeker

Đúng. Biến thể gõ yếu với loại lưu trữ được chỉ định thêm các hằng số của nó vào cả phạm vi kèm theo phạm vi riêng của nó, iiuc.
Vô dụng

Điều tra viên không được kiểm soát chỉ được khai báo trong phạm vi xung quanh. Có thể đủ điều kiện bằng tên enum là một phần của quy tắc tra cứu, không phải là tuyên bố. C ++ 11 7.2 / 10: Mỗi tên enum và mỗi điều tra viên không bị chặn được khai báo trong phạm vi có chứa ngay enum-specifier. Mỗi điều tra viên phạm vi được khai báo trong phạm vi liệt kê. Các tên này tuân theo các quy tắc phạm vi được xác định cho tất cả các tên trong (3.3) và (3.4).
Lars Viklund

1
với C ++ 11, chúng ta có std :: ngầm_type cung cấp kiểu cơ bản của enum. Vì vậy, chúng ta có 'template <typename IntegralType> struct Integral {typedef typename std :: under_type <IntegralType> :: type Type; }; `Trong C ++ 14, những thứ này thậm chí còn đơn giản hơn để không nhìn thấy <tên kiểu IntegralType> struct Integral {typedef std :: ngầm_type_t <IntegralType> Type; };
emsr

4

Bạn có thể định nghĩa các cờ enum loại an toàn trong C ++ 11 bằng cách sử dụng std::enable_if. Đây là một triển khai thô sơ có thể thiếu một số điều:

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;
}

Lưu ý rằng number_of_bitskhông may không thể điền vào trình biên dịch, vì C ++ không có cách nào để xem xét các giá trị có thể có của một bảng liệt kê.

Chỉnh sửa: Trên thực tế tôi đứng sửa, có thể để trình biên dịch điền number_of_bitscho bạn.

Lưu ý điều này có thể xử lý (cực kỳ không hiệu quả) một phạm vi giá trị enum không liên tục. Chúng ta hãy nói rằng không nên sử dụng những điều trên với một enum như thế này hoặc sự điên rồ sẽ xảy ra:

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

Nhưng tất cả những thứ được coi là một giải pháp khá hữu dụng cuối cùng. Không cần bất kỳ hoạt động bitfiddling nào của người dùng, là loại an toàn và trong giới hạn của nó, hiệu quả như nó có được (Tôi đang nghiêng về std::bitsetchất lượng thực hiện ở đây ;)).


Tôi chắc chắn tôi đã bỏ lỡ một số quá tải của các nhà khai thác.
rubenvb

2

Tôi ghét ghét macro trong C ++ 14 của tôi nhiều như anh chàng tiếp theo, nhưng tôi đã sử dụng nó ở mọi nơi và cũng khá tự do:

#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

Sử dụng đơn giản như

ENUM_FLAGS(Fish)
{
    OneFish,
    TwoFish,
    RedFish,
    BlueFish
};

Và, như họ nói, bằng chứng là trong bánh pudding:

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;
}

Hãy thoải mái xác định bất kỳ toán tử riêng lẻ nào khi bạn thấy phù hợp, nhưng theo ý kiến ​​rất thiên vị của tôi, C / C ++ là để can thiệp vào các khái niệm và luồng cấp thấp, và bạn có thể loại bỏ các toán tử bitwise này khỏi bàn tay lạnh lẽo, chết chóc của tôi và tôi sẽ chiến đấu với bạn bằng tất cả các macro không linh hoạt và các phép thuật lật bit mà tôi có thể tạo ra để giữ chúng.


2
Nếu bạn ghét macro nhiều như vậy, tại sao không sử dụng cấu trúc C ++ thích hợp và viết một số toán tử mẫu thay vì macro? Có thể cho rằng, cách tiếp cận mẫu là tốt hơn bởi vì bạn có thể sử dụng std::enable_ifvới std::is_enumhạn chế quá tải toán tử miễn phí của bạn chỉ làm việc với các loại được liệt kê. Tôi cũng đã thêm các toán tử so sánh (sử dụng std::underlying_type) và toán tử không logic để thu hẹp khoảng cách mà không làm mất kiểu gõ mạnh. Điều duy nhất tôi không thể phù hợp là chuyển đổi ngầm để bool, nhưng flags != 0!flagsđủ đối với tôi.
khỉ0506

1

Thông thường, bạn xác định một tập hợp các giá trị nguyên tương ứng với các số nhị phân của tập hợp một bit, sau đó cộng chúng lại với nhau. Đây là cách lập trình viên C thường làm.

Vì vậy, bạn sẽ có (sử dụng toán tử bithift để đặt các giá trị, ví dụ 1 << 2 giống như nhị phân 100)

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

Vân vân

Trong C ++, bạn có nhiều tùy chọn hơn, xác định một loại mới thay vì int (sử dụng typedef ) và đặt các giá trị tương tự như trên; hoặc xác định một bitfield hoặc một vectơ của bools . 2 cái cuối cùng rất hiệu quả về không gian và có ý nghĩa hơn rất nhiều khi xử lý cờ. Một bitfield có lợi thế là cung cấp cho bạn loại kiểm tra (và do đó là intellisense).

Tôi muốn nói (rõ ràng là chủ quan) rằng một lập trình viên C ++ nên sử dụng bitfield cho vấn đề của bạn, nhưng tôi có xu hướng thấy cách tiếp cận #define được các chương trình C sử dụng rất nhiều trong các chương trình C ++.

Tôi cho rằng bitfield là gần nhất với enum của C #, tại sao C # cố gắng quá tải một enum thành một loại bitfield là lạ - một enum thực sự phải là loại "chọn một lần".


11
sử dụng macro trong c ++ theo cách như vậy là không tốt
BЈовић

3
C ++ 14 cho phép bạn xác định các chữ nhị phân (ví dụ 0b0100) để 1 << nđịnh dạng bị lỗi thời.
Rob K

Có lẽ bạn có nghĩa là bitet thay vì bitfield.
Jorge Bellon

1

Một ví dụ ngắn về cờ enum bên dưới, trông khá giống C #.

Về cách tiếp cận, theo tôi: ít mã hơn, ít lỗi hơn, mã tốt hơn.

#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) là một macro, được xác định trong enum_flags.h (ít hơn 100 dòng, miễn phí sử dụng mà không bị hạn chế).


1
là tệp enum_flags.h giống như trong lần sửa đổi đầu tiên của câu hỏi của bạn? nếu có, bạn có thể sử dụng URL sửa đổi để tham khảo: http://programmers.stackexchange.com/revutions/205567/1
gnat

+1 có vẻ tốt, sạch sẽ. Tôi sẽ thử điều này trong dự án SDK của chúng tôi.
Garet Claborn

1
@GaretClaborn Đây là những gì tôi gọi là sạch: paste.ubfox.com/23883996
sehe

1
Tất nhiên, bỏ lỡ ::typeở đó. Đã sửa lỗi
sehe

@sehe hey, mã mẫu không được coi là dễ đọc và có ý nghĩa. phù thủy này là gì? thật tuyệt .... đoạn trích này có mở để sử dụng lol không
Garet Claborn

0

Vẫn còn một cách khác để lột da mèo:

Thay vì làm quá tải các toán tử bit, ít nhất một số có thể thích chỉ thêm một lớp lót 4 để giúp bạn tránh được sự hạn chế khó chịu của các enum có phạm vi:

#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;
}

Cấp, bạn phải gõ ut_cast()điều đó mỗi lần, nhưng về mặt tăng, điều này mang lại mã dễ đọc hơn, theo nghĩa tương tự như sử dụng static_cast<>(), so với chuyển đổi loại ngầm hoặc operator uint16_t()loại điều.

Và hãy trung thực ở đây, sử dụng kiểu Foonhư trong đoạn mã trên có những mối nguy hiểm của nó:

Ở một nơi khác, ai đó có thể thực hiện một trường hợp chuyển đổi qua biến foovà không mong đợi rằng nó chứa nhiều hơn một giá trị ...

Vì vậy, xả rác mã ut_cast()giúp cảnh báo người đọc rằng một cái gì đó tanh đang xảy ra.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.