Menggunakan enum yang dicakup untuk bendera bit di C ++

60

Sebuah enum X : int(C #) atau enum class X : int(C ++ 11) adalah jenis yang memiliki medan batin tersembunyi intyang dapat menahan nilai apapun. Selain itu, sejumlah konstanta yang Xtelah ditentukan didefinisikan pada enum. Dimungkinkan untuk melemparkan enum ke nilai integernya dan sebaliknya. Ini semua benar dalam C # dan C ++ 11.

Dalam C # enums tidak hanya digunakan untuk menyimpan nilai-nilai individual, tetapi juga untuk menahan kombinasi flag bitwise, sesuai rekomendasi Microsoft . Enum semacam itu (biasanya, tetapi tidak harus) didekorasi dengan [Flags]atribut. Untuk membuat kehidupan pengembang lebih mudah, operator bitwise (OR, AND, dll ...) kelebihan beban sehingga Anda dapat dengan mudah melakukan sesuatu seperti ini (C #):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Saya seorang pengembang C # yang berpengalaman, tetapi telah memprogram C ++ hanya untuk beberapa hari sekarang, dan saya tidak dikenal dengan konvensi C ++. Saya bermaksud menggunakan enum C ++ 11 dengan cara yang sama persis seperti yang biasa saya lakukan di C #. Dalam C ++ 11 operator bitwise pada scoped enums tidak kelebihan beban, jadi saya ingin membebani mereka .

Ini mengundang debat, dan pendapat tampaknya bervariasi di antara tiga opsi:

  1. Variabel tipe enum digunakan untuk menahan bidang bit, mirip dengan 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)));
    

    Tapi ini akan melawan filosofi enum yang sangat diketik dari enum C ++ 11.

  2. Gunakan bilangan bulat polos jika Anda ingin menyimpan kombinasi bit enum:

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

    Tapi ini akan mengurangi segalanya menjadi int, meninggalkan Anda tanpa petunjuk tentang jenis apa yang harus Anda masukkan ke dalam metode.

  3. Tulis kelas terpisah yang akan membebani operator dan menahan bendera bitwise di bidang bilangan bulat tersembunyi:

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

    ( kode lengkap oleh pengguna315052 )

    Tetapi kemudian Anda tidak memiliki IntelliSense atau dukungan apa pun untuk mengisyaratkan Anda pada nilai-nilai yang mungkin.

Saya tahu ini adalah pertanyaan subyektif , tetapi: Pendekatan apa yang harus saya gunakan? Pendekatan apa, jika ada, yang paling dikenal di C ++? Pendekatan apa yang Anda gunakan ketika berhadapan dengan bidang bit dan mengapa ?

Tentu saja karena ketiga pendekatan ini berhasil, saya mencari alasan faktual dan teknis, konvensi yang diterima secara umum, dan bukan hanya preferensi pribadi.

Sebagai contoh, karena latar belakang C # saya cenderung pergi dengan pendekatan 1 di C ++. Ini memiliki manfaat tambahan bahwa lingkungan pengembangan saya dapat memberi saya petunjuk tentang nilai-nilai yang mungkin, dan dengan operator enum yang berlebihan ini mudah untuk ditulis dan dipahami, dan cukup bersih. Dan metode tanda tangan menunjukkan dengan jelas nilai seperti apa yang diharapkannya. Tetapi kebanyakan orang di sini tidak setuju dengan saya, mungkin karena alasan yang baik.

Daniel AA Pelsmaeker
sumber
2
Komite ISO C ++ menemukan opsi 1 cukup penting untuk secara eksplisit menyatakan bahwa rentang nilai enum mencakup semua kombinasi biner dari flag. (Ini sebelum C ++ 03) Jadi ada persetujuan objektif dari pertanyaan yang agak subjektif ini.
MSalters
1
(Untuk mengklarifikasi komentar @MSalters, rentang C ++ enum didasarkan pada tipe dasarnya (jika tipe tetap), atau sebaliknya pada enumeratornya. Dalam kasus terakhir, rentang didasarkan pada bitfield terkecil yang dapat menampung semua enumerator yang ditentukan ; misalnya, untuk enum E { A = 1, B = 2, C = 4, };, kisarannya adalah 0..7(3 bit). Dengan demikian, standar C ++ secara eksplisit menjamin bahwa # 1 akan selalu menjadi opsi yang layak. [Secara khusus, enum classdefault ke enum class : intkecuali ditentukan lain, dan dengan demikian selalu memiliki tipe yang mendasari tetap.])
Justin Time 2 Reinstate Monica

Jawaban:

31

Cara paling sederhana adalah dengan memberikan operator kelebihan beban sendiri. Saya berpikir untuk membuat makro untuk memperluas kelebihan beban dasar per jenis.

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

(Perhatikan bahwa itu type_traitsadalah header C ++ 11 dan std::underlying_type_tmerupakan fitur C ++ 14.)

Dave
sumber
6
std :: underlying_type_t adalah C ++ 14. Dapat menggunakan std :: underlying_type <T> :: ketik C ++ 11.
ddevienne
14
Mengapa Anda menggunakan static_cast<T>input, tetapi cor gaya C untuk hasil di sini?
Ruslan
2
@Ruslan Saya pertanyaan kedua ini
audiFanatic
Mengapa Anda repot-repot dengan std :: underlying_type_t ketika Anda sudah tahu itu int?
poizan42
1
Jika SBJFrameDragdidefinisikan dalam kelas dan |-operator kemudian digunakan dalam definisi kelas yang sama, bagaimana Anda mendefinisikan operator sehingga dapat digunakan di dalam kelas?
HelloGoodbye
6

Secara historis, saya akan selalu menggunakan enumerasi lama (yang diketik dengan lemah) untuk memberi nama konstanta bit, dan hanya menggunakan kelas penyimpanan secara eksplisit untuk menyimpan flag yang dihasilkan. Di sini, tanggung jawab akan berada pada saya untuk memastikan enumerasi saya sesuai dengan jenis penyimpanan, dan untuk melacak hubungan antara bidang dan konstanta terkait.

Saya menyukai gagasan enum yang sangat diketik, tetapi saya tidak terlalu nyaman dengan gagasan bahwa variabel jenis yang dihitung mungkin mengandung nilai yang tidak termasuk dalam konstanta enumerasi itu.

Misalnya, dengan asumsi bitwise atau telah kelebihan beban:

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

Untuk opsi ke-3 Anda, Anda memerlukan beberapa boilerplate untuk mengekstraksi tipe penyimpanan enumerasi. Dengan asumsi kami ingin memaksakan tipe dasar yang tidak ditandatangani (kami juga dapat menangani yang ditandatangani, dengan sedikit kode lagi):

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;

Ini masih tidak memberi Anda IntelliSense atau pelengkapan otomatis, tetapi deteksi tipe penyimpanan kurang jelek dari yang saya harapkan.


Sekarang, saya memang menemukan alternatif: Anda dapat menentukan tipe penyimpanan untuk enumerasi yang diketik dengan lemah. Bahkan memiliki sintaks yang sama seperti di C #

enum E4 : int { ... };

Karena ini diketik dengan lemah, dan secara implisit mengkonversi ke / dari int (atau jenis penyimpanan apa pun yang Anda pilih), rasanya kurang aneh untuk memiliki nilai yang tidak cocok dengan konstanta yang disebutkan.

The downside adalah bahwa ini digambarkan sebagai "transisi" ...

NB. varian ini menambahkan konstanta enumerasi pada ruang lingkup bersarang dan terlampir, tetapi Anda bisa mengatasinya dengan namespace:

namespace E5 {
    enum Enum : int { A, B, C };
}
E5::Enum x = E5::A; // or E5::Enum::A
Tak berguna
sumber
1
Kelemahan lain dari enum yang diketik dengan lemah adalah bahwa konstanta mereka mencemari namespace saya, karena mereka tidak perlu diawali dengan nama enum. Dan itu juga dapat menyebabkan semua jenis perilaku aneh jika Anda memiliki dua enum yang berbeda baik dengan anggota dengan nama yang sama.
Daniel AA Pelsmaeker
Itu benar. Varian dengan tipe yang lemah dengan tipe penyimpanan yang ditentukan menambah konstanta pada lingkup penutup dan cakupannya sendiri, iiuc.
berguna
Pencacah yang tidak dicentang hanya dideklarasikan dalam lingkup sekitarnya. Mampu memenuhi syarat dengan nama enum adalah bagian dari aturan pencarian, bukan deklarasi. C ++ 11 7.2 / 10: Setiap enum-name dan setiap enumerator yang tidak dicopot dinyatakan dalam lingkup yang segera berisi enum-specifier. Setiap pencacah tercakup dinyatakan dalam ruang lingkup pencacahan. Nama-nama ini mematuhi aturan ruang lingkup yang ditentukan untuk semua nama dalam (3.3) dan (3.4).
Lars Viklund
1
dengan C ++ 11 kita memiliki std :: underlying_type yang menyediakan tipe enum yang mendasarinya. Jadi kita memiliki 'template <typename IntegralType> struct Integral {typedef typename std :: underlying_type <IntegralType> :: type Type; }; `Dalam C ++ 14 ini lebih disederhanakan untuk memulai <typename IntegralType> struct Integral {typedef std :: underlying_type_t <IntegralType> Type; };
emsr
4

Anda dapat mendefinisikan flag enum tipe-aman di C ++ 11 dengan menggunakan std::enable_if. Ini adalah implementasi yang belum sempurna yang mungkin kehilangan beberapa hal:

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

Catatan number_of_bitssayangnya tidak dapat diisi oleh kompiler, karena C ++ tidak memiliki cara untuk melakukan introspeksi nilai-nilai yang mungkin dari sebuah enumerasi.

Sunting: Sebenarnya saya berdiri dikoreksi, adalah mungkin untuk mendapatkan isi kompiler number_of_bitsuntuk Anda.

Catatan ini dapat menangani (sangat tidak efisien) rentang nilai enum yang tidak berkelanjutan. Katakan saja itu bukan ide yang baik untuk menggunakan hal di atas dengan enum seperti ini atau kegilaan akan terjadi:

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

Tetapi semua hal yang dianggap sebagai solusi pada akhirnya cukup bermanfaat. Tidak memerlukan bitfiddling sisi pengguna, tipe-aman dan dalam batas-batasnya, seefisien yang didapat (Saya sangat bergantung pada std::bitsetkualitas implementasi di sini ;)).

rubenvb
sumber
Saya yakin saya melewatkan beberapa kelebihan operator.
rubenvb
2

saya benci membenci makro di C ++ 14 saya sebanyak orang berikutnya, tetapi saya telah menggunakan ini di mana-mana, dan juga secara liberal:

#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

Membuat penggunaan sesederhana

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

Dan, seperti yang mereka katakan, buktinya ada di puding:

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

Jangan ragu untuk mendefinisikan salah satu dari operator individual yang Anda inginkan, tetapi menurut pendapat saya yang sangat bias, C / C ++ adalah untuk berinteraksi dengan konsep dan aliran tingkat rendah, dan Anda dapat mencabut operator bitwise ini dari tangan saya yang dingin dan mati. dan aku akan bertarung denganmu dengan semua makro yang tidak suci dan mantra yang bisa kubalik untuk menjaga mereka.

Mahmoud Al-Qudsi
sumber
2
Jika Anda sangat membenci makro, mengapa tidak menggunakan konstruksi C ++ yang tepat dan menulis beberapa operator templat alih-alih makro? Dapat diperdebatkan, pendekatan template lebih baik karena Anda dapat menggunakan std::enable_ifdengan std::is_enumuntuk membatasi kelebihan operator Anda hanya untuk bekerja dengan tipe yang disebutkan. Saya juga menambahkan operator perbandingan (menggunakan std::underlying_type) dan operator bukan logis untuk lebih menjembatani kesenjangan tanpa kehilangan pengetikan yang kuat. Satu-satunya hal yang saya tidak dapat mencocokkan adalah konversi implisit ke bool, tetapi flags != 0dan !flagscukup bagi saya.
monkey0506
1

Biasanya Anda akan mendefinisikan satu set nilai integer yang sesuai dengan angka-angka biner bit-set tunggal, kemudian menambahkannya bersama-sama. Ini adalah cara programmer C biasanya melakukannya.

Jadi Anda harus (menggunakan operator bitshift untuk mengatur nilai, misalnya 1 << 2 sama dengan biner 100)

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

dll

Di C ++ Anda memiliki lebih banyak opsi, tentukan jenis baru dan bukan int (gunakan typedef ) dan tetapkan nilai seperti di atas; atau mendefinisikan bitfield atau vektor bools . 2 yang terakhir sangat efisien ruang dan jauh lebih masuk akal untuk berurusan dengan bendera. Bitfield memiliki keuntungan memberi Anda tipe pengecekan (dan karenanya, intellisense).

Saya akan mengatakan (jelas subyektif) bahwa seorang programmer C ++ harus menggunakan bitfield untuk masalah Anda, tetapi saya cenderung melihat pendekatan #define yang sering digunakan oleh program C dalam program C ++.

Saya kira bitfield adalah yang paling dekat dengan enum C #, mengapa C # mencoba untuk membebani enum menjadi tipe bitfield itu aneh - enum harus benar-benar menjadi tipe "pilihan tunggal".

gbjbaanb
sumber
11
menggunakan macro di c ++ sedemikian rupa buruk
BЈовић
3
C ++ 14 memungkinkan Anda untuk mendefinisikan literal biner (misalnya 0b0100) sehingga 1 << nformatnya sudah usang.
Rob K
Mungkin maksud Anda bitset, bukan bitfield.
Jorge Bellon
1

Contoh singkat dari enum-flag di bawah ini, terlihat sangat mirip dengan C #.

Tentang pendekatan, menurut pendapat saya: lebih sedikit kode, lebih sedikit bug, kode lebih baik.

#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) adalah makro, didefinisikan dalam enum_flags.h (kurang dari 100 baris, gratis untuk digunakan tanpa batasan).

Yuri Yaryshev
sumber
1
Apakah file enum_flags.h sama dengan revisi pertama pertanyaan Anda? jika ya, Anda dapat menggunakan URL revisi untuk merujuknya: http://programmers.stackexchange.com/revisions/205567/1
gnat
+1 terlihat bagus, bersih. Saya akan mencoba ini di proyek SDK kami.
Garet Claborn
1
@ GaretClaborn Inilah yang saya sebut clean: paste.ubuntu.com/23883996
sehe
1
Tentu saja, ketinggalan di ::typesana. Diperbaiki: paste.ubuntu.com/23884820
sehe
@sehe hei, kode templat tidak seharusnya terbaca dan masuk akal. apa sihir ini? bagus .... apakah cuplikan ini terbuka untuk menggunakan lol
Garet Claborn
0

Masih ada cara lain untuk menguliti kucing:

Alih-alih membebani operator bit, setidaknya beberapa mungkin lebih suka untuk hanya menambahkan 4 liner untuk membantu Anda menghindari pembatasan yang buruk dari enum yang dicakup:

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

Memang, Anda harus mengetikkan ut_cast()hal itu setiap kali, tetapi di sisi atas, ini menghasilkan kode yang lebih mudah dibaca, dalam arti yang sama seperti menggunakan static_cast<>(), dibandingkan dengan konversi jenis implisit atau operator uint16_t()semacamnya.

Dan mari kita jujur ​​di sini, menggunakan tipe Fooseperti pada kode di atas memiliki bahayanya:

Di tempat lain seseorang mungkin melakukan case switch lebih dari variabel foodan tidak berharap itu memegang lebih dari satu nilai ...

Jadi mengotori kode dengan ut_cast()membantu mengingatkan pembaca bahwa ada sesuatu yang mencurigakan sedang terjadi.

BitTickler
sumber