C ++ 'ta bit bayrakları için scoped enums kullanmak


60

Bir enum X : int(C #) veya enum class X : int(C ++ 11), intherhangi bir değeri tutabilecek gizli bir iç alanına sahip olan bir türdür . Ek olarak, bir dizi önceden tanımlanmış sabit Xenumda tanımlanmıştır. Enum değerini tamsayı değerine çevirmek mümkündür; Tüm bunlar hem C # hem de C ++ 11 için geçerlidir.

C # enums değerleri yalnızca bireysel değerleri tutmak için değil, aynı zamanda Microsoft’un önerisine göre bitsel bayrak kombinasyonlarını tutmak için de kullanılır . Bu tür numaralandırmalar (genellikle, ancak zorunlu değil) [Flags]özellik ile dekore edilir . Geliştiricilerin hayatlarını kolaylaştırmak için, bitsel operatörler (VEYA, VE, vb.) Aşırı yüklenir, böylece kolayca böyle bir şey yapabilirsiniz (C #):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Ben tecrübeli bir C # geliştiricisiyim, ancak birkaç gündür C ++ 'ı programladım ve C ++ sözleşmeleri ile bilmiyorum. C ++ 11 enum C # ile aynı şekilde kullanmak niyetindeyim. C ++ 11'de, geniş kapsamlı operatörlerdeki bitsel operatörler aşırı yüklenmez, bu yüzden onları aşırı yüklemek istedim .

Bu bir tartışma istedi ve görüşler üç seçenek arasında değişiyor gibi görünüyor:

  1. Enum türünün bir değişkeni, bit alanını tutmak için kullanılır, C #'ya benzer:

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

    Ancak bu, C ++ 11'in kapsamındaki enum'ların kuvvetle yazılmış enum felsefesine aykırı olacaktır.

  2. Bitsel bir enums kombinasyonunu saklamak istiyorsanız düz bir tamsayı kullanın:

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

    Fakat bu, her şeyi azaltacaktır int, sizi metoda hangi türden koymanız gerektiğine dair hiçbir ipucu olmadan bırakacaktı.

  3. Operatörleri aşırı yükleyecek ve gizli bir tamsayı alanında bitsel bayrakları tutacak ayrı bir sınıf yazı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);
    

    ( user315052 tarafından tam kod )

    Ancak daha sonra IntelliSense'e veya sizi olası değerlere işaret edecek herhangi bir desteğe sahip değilsiniz.

Bunun öznel bir soru olduğunu biliyorum ama: Hangi yaklaşımı kullanmalıyım? C ++ 'da en yaygın olarak bilinen yaklaşım hangisidir? Bit alanları ile uğraşırken hangi yaklaşımı kullanıyorsunuz ve neden ?

Elbette her üç yaklaşım da işe yaradığından beri, kişisel tercihlerden değil, genel ve kabul görmüş sözleşmelerden oluşan gerçek ve teknik sebepler arıyorum.

Örneğin, C # arkaplanım nedeniyle C ++ 'da yaklaşım 1' e gitme eğilimindeyim. Bu, geliştirme ortamımın beni muhtemel değerler konusunda ipuçlarına sokabileceğini ve aşırı yüklü enum operatörleriyle bunun yazılması ve anlaşılması kolay ve oldukça temiz olduğunu da ekledi. Ve yöntem imzası, ne tür bir değer beklediğini açıkça göstermektedir. Fakat buradaki çoğu insan muhtemelen iyi bir sebepten dolayı benimle aynı fikirde değil.


2
ISO C ++ komitesi, enums değer aralığının tüm ikili bayrak kombinasyonlarını içerdiğini açıkça belirtecek kadar seçenek 1'i önemli buldu. (Bu, C ++ 03'ten önce gelir) Yani bu biraz öznel sorunun nesnel bir onayı var.
MSalters

1
(@MSalters yorumunu netleştirmek için, bir C ++ enum'un aralığı, temel türüne (eğer sabit bir tür ise) ya da başka türlü numaralandırıcılarına dayanır. İkinci durumda, aralık, tüm tanımlanmış numaralandırıcıları tutabilen en küçük bit alanına dayanır Örneğin, enum E { A = 1, B = 2, C = 4, };aralık için 0..7(3 bit) C ++ standardı açıkça # 1'in daima uygulanabilir bir seçenek olacağını garanti eder. [Özellikle, aksi belirtilmediği sürece enum classvarsayılan enum class : intolarak belirlenir ve dolayısıyla her zaman sabit bir alt tipe sahiptir.])
Justin Time

Yanıtlar:


31

En basit yol, operatörün kendini aşırı yüklemesini sağlamaktır. Tür başına temel aşırı yükleri genişletmek için bir makro oluşturmayı düşünüyorum.

#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_traitsC ++ 11 üstbilgisi ve std::underlying_type_tC ++ 14 özelliği olduğunu unutmayın.)


6
std :: underlying_type_t C ++ 14'tür. C ++ 11'de std :: underlying_type <T> :: tipini kullanabilir.
ddevienne

14
Neden static_cast<T>giriş için kullanıyorsunuz ama sonuçta C-tarzı yayınlanıyor?
Ruslan

2
@Ruslan Bu ikinci soruyu
audiFanatic

Neden int olduğunu bildiğiniz halde neden std :: underlying_type_t ile uğraşıyorsunuz?
poizan42

1
Eğer SBJFrameDragbir sınıfta tanımlanır ve |-Operatör sonra aynı sınıfın tanımlarında kullanılan, nasıl bu sınıf içinde kullanılabilecek şekilde operatörü tanımlarsınız?
HelloGoodbye

6

Tarihsel olarak, her zaman eski (zayıf yazılmış) numaralandırmayı bit sabitlerini adlandırmak için kullanırdım ve sadece elde edilen bayrağın depolanması için açıkça depolama sınıfını kullanırdım. Burada, sayımlarımın depolama türüne uyduğundan emin olmak ve alan ile ilgili sabitler arasındaki ilişkinin kaydını tutmak için benim üzerimde olacaktı.

Kesin olarak yazılmış numaralar fikrini severim, ancak numaralandırılmış türdeki değişkenlerin bu numaralandırma sabitleri arasında olmayan değerler içerebileceği fikrinden gerçekten rahat değilim.

Örneğin, bitsel veya aşırı yüklenmiş olarak varsaymak:

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

3. seçeneğiniz için, numaralandırmanın depolama türünü çıkarmak için bir kazan plakasına ihtiyacınız var. İmzasız bir temel tür zorlamak istediğimizi varsayarsak (biraz daha fazla kodla da imzalanmış olarak işleyebiliriz):

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;

Bu size IntelliSense veya otomatik tamamlama özelliğini vermiyor, ancak depolama türü tespiti başlangıçta beklediğimden daha az çirkin.


Şimdi, bir alternatif buldum: zayıf yazılmış bir numaralandırma için depolama türünü belirleyebilirsiniz. Hatta C # ile aynı sözdizimine sahiptir.

enum E4 : int { ... };

Zayıf yazıldığından ve dolaylı olarak int'den / int'ye (veya seçtiğiniz depolama türüne) dönüştürüldüğü için, numaralandırılan sabitlerle eşleşmeyen değerlere sahip olmak daha az garip hissettirir.

Dezavantajı ise bunun "geçiş" olarak tanımlanmasıdır ...

NB. bu değişken numaralandırılmış sabitlerini hem iç içe hem de kapsam kapsamına ekler, ancak bu konuda bir ad alanıyla çalışabilirsiniz:

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

1
Zayıf yazılan numaraların bir başka dezavantajı, sabitlerinin ad alanımı kirlettiğidir; Ayrıca, her ikisi de aynı ada sahip iki farklı numaraya sahipseniz, bu her tür garip davranışa neden olabilir.
Daniel AA Pelsmaeker

Bu doğru. Belirtilen depolama türü ile zayıf-daktilo varyant çevreleyen kapsam hem kendi sabitlerini ekler ve kendi kapsamı, iiuc.
Yararsız

Kaplamasız numaralandırıcı yalnızca çevre kapsamında bildirilir. Bunu enum adıyla niteleyebilmek, beyan kurallarının değil, arama kurallarının bir parçasıdır. C ++ 11 7.2 / 10: Her enum adı ve her kapatılmamış numaralandırıcı derhal enum-tanımlayıcısını içeren kapsamda bildirilir. Her kapsamda numaralandırıcı numaralandırma kapsamında ilan edilir. Bu isimler (3.3) ve (3.4) 'te tüm isimler için tanımlanan kapsam kurallarına uyar.
Lars Viklund

1
C ++ 11 ile temel bir enum türü sağlayan std :: underlying_type var. Bu yüzden biz 'template <typename IntegralType> struct Integral {typedef typename std :: underlying_type <IntegralType> :: type Type; }; `C ++ 14'te bunlar, <typename IntegralType> struct Integral {typedef std :: underlying_type_t <IntegralType> Type; };
emsr

4

Güvenli yazı tipi bayraklarını C ++ 11 kullanarak tanımlayabilirsiniz std::enable_if. Bu, bazı şeyleri eksik olabilecek ilkel bir uygulamadır:

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 ++ 'ın bir sayımın olası değerlerini gözden geçirmek için herhangi bir yolu olmadığı için ne yazık ki derleyici tarafından doldurulmadığını unutmayın .

Düzenleme: Aslında düzeltilmiş duruyorum, derleyiciyi number_of_bitssizin için doldurmak mümkündür .

Bunun sürekli olmayan bir enum değeri aralığını işleyebileceğini (çılgınca verimsiz) yapabileceğini unutmayın. Diyelim ki yukarıdakileri böyle bir enumla kullanmanın iyi bir fikir olmadığını söyleyelim, yoksa delilik ortaya çıkar:

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

Ancak bununla ilgili her şey sonuçta oldukça kullanışlı bir çözüm. Kullanıcı tarafında herhangi bir bitfiddling gerekmiyor, tip güvenli ve sınırları içinde, olabildiğince verimli ( std::bitsetburada uygulama kalitesine kuvvetli bir şekilde yaslanıyorum ;)).


Operatörlerin aşırı yükünü kaçırdığına eminim.
rubenvb

2

ben nefret C ++ 14'ümde bir sonraki adam kadar makro çekmemiştim, ama bunu her yerde kullanmaya başladım ve oldukça liberal olarak:

#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

Kullanımı kadar basit

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

Ve dedikleri gibi, kanıt pudingte:

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

Tek tek operatörleri tanımlamaktan çekinmeyin, uygun gördüğünüz gibi, ama çok taraflı görüşüme göre, C / C ++ düşük seviyeli konseptler ve akışlarla etkileşimde bulunmak içindir ve bu bitsel operatörleri soğuk, ölü ellerimden çıkarabilirsiniz. ve onları saklamak için harcayabileceğim tüm kutsal olmayan makrolar ve biraz saygısız büyülerle seninle savaşacağım.


2
Makroları çok fazla zorlarsanız, neden uygun bir C ++ yapısı kullanıp makro yerine bazı şablon operatörleri yazmıyorsunuz? Kullanmak çünkü Muhtemelen, şablon yaklaşım iyidir std::enable_ifile std::is_enumsadece numaralandırılan tipleri ile çalışmak için ücretsiz operatör aşırı yükleri kısıtlamak için. Ben de std::underlying_typegüçlü yazarak kaybetmeden boşluğu daha fazla köprü için karşılaştırma operatörleri (kullanarak ) ve mantıksal olmayan operatör ekledik . Ben eşleşmiyor tek şey bool için örtük dönüştürme, ancak flags != 0ve !flagsbenim için yeterlidir.
maymun0506

1

Genellikle, tek bitlik ayarlanmış ikili sayılara karşılık gelen bir tamsayı değerleri tanımlayıp ardından bunları bir araya eklersiniz. Bu, C programcılarının genellikle yaptığı yoldur.

Yani (değerleri ayarlamak için bitshift operatörünü kullanarak, örneğin 1 << 2 ikili 100 ile aynıdır)

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

vb

C ++ 'ta daha fazla seçeneğiniz vardır, bunun yerine bir int (yerine typedef kullanın ) ve benzer şekilde yukarıdaki değerleri ayarlayın; veya bir bit alanını veya bir bools vektörünü tanımlayın . Son 2 alan çok verimli ve bayraklarla uğraşmak için çok daha mantıklı. Bir bit alanı size tip kontrolü sağlama avantajına sahiptir (ve bu nedenle intellisense).

Bir C ++ programlayıcısının probleminiz için bir bit alanı kullanması gerektiğini (açıkçası öznel) söyleyebilirim, ancak C programları tarafından C ++ programlarında kullanılan #define yaklaşımını görmeye meyilliyim.

Galiba bitfield C # enum değerine en yakın, neden C # bir bitfield tipi olmak için bir enum'u aşırı yüklemeye çalıştığım garipti - bir enum gerçekten "tek seçim" tip olmalı.


11
c ++ makrolarını bu şekilde kullanmak kötü
BЈовић

3
C ++ 14, ikili değişmezleri (örn. 0b0100) Tanımlamanıza izin verir, böylece 1 << nbiçim eskidir.
Rob K

Belki de bitfield yerine bit setini kastettin .
Jorge Bellon

1

Aşağıdaki enum bayrakları kısa bir örneği, C # gibi görünüyor.

Yaklaşım hakkında, bence: daha az kod, daha az hata, daha iyi kod.

#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 içinde tanımlanmış bir makrodur (100 satırdan az, kısıtlama olmadan kullanmak ücretsizdir).


1
enum_flags.h dosyası, sorunuzun 1. revizyonundakiyle aynı mı? evet ise, başvurmak için revizyon URL’yi kullanabilirsiniz: http://programmers.stackexchange.com/revisions/205567/1
gnat

+1 iyi görünüyor, temiz. Bunu SDK projemizde deneyeceğim.
Garet Claborn,

1
@GaretClaborn Ben buna temiz derim
17:17

1
Tabii ki, ::typeorada özledim . Sabit: paste.ubuntu.com/23884820
sehe

@ hey, şablon kodunun okunaklı olması ve mantıklı olması gerekmiyor. bu büyücülük nedir? güzel .... bu pasajı lol kullanmaya açık
Garet Claborn

0

Kedinin derisini soymanın başka bir yolu var:

Bit işleçlerini aşırı yüklemek yerine, en azından bazıları, geniş kapsamlı kapsam kısıtlamalarını aşmanıza yardımcı olmak için sadece 4 astar eklemek isteyebilir:

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

Verilmiş, ut_cast()her seferinde bir şey yazmanız gerekir , ancak üst tarafta bu static_cast<>(), örtük tip dönüşümü veya operator uint16_t()benzeri şeylerle karşılaştırıldığında , aynı anlamda, daha okunabilir bir kod verir .

Ve burada dürüst olalım, Fooyukarıdaki koddaki gibi bir yazı kullanmanın tehlikeleri vardır:

Başka bir yerde birileri değişken üzerinde geçiş davası açabilir foove birden fazla değer beklemesini beklemeyebilir ...

Bu yüzden kodu ut_cast()bir yere koymak, okuyucuların balıkların sürmekte olduğunun farkına varmasına yardımcı olur.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.