Usando enums com escopo definido para sinalizadores de bit em C ++

53

Um enum X : int (C #) ou enum class X : int (C ++ 11) é um tipo que possui um campo interno oculto de int que pode conter qualquer valor. Além disso, um número de constantes predefinidas de X é definido no enum. É possível converter o enum para seu valor inteiro e vice-versa. Isso é tudo verdade em C # e C ++ 11.

Em enums C # não são usados apenas para manter valores individuais, mas também para conter combinações de sinalizadores bit a bit, conforme recomendação da Microsoft . Essas enumerações são (geralmente, mas não necessariamente) decoradas com o atributo [Flags] . Para facilitar a vida dos desenvolvedores, os operadores bitwise (OR, AND, etc ...) são sobrecarregados para que você possa facilmente fazer algo assim (C #):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Eu sou um desenvolvedor C # experiente, mas tenho programado o C ++ apenas por alguns dias agora, e não sou conhecido com as convenções do C ++. Eu pretendo usar um enum C ++ 11 da mesma maneira que eu estava acostumado a fazer em C #. Em C ++ 11, os operadores bitwise em enums com escopo definido não estão sobrecarregados, então eu queria sobrecarregá-los .

Isso solicitou um debate, e as opiniões parecem variar entre três opções:

  1. Uma variável do tipo enum é usada para conter o campo de bits, semelhante ao 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)));
    

    Mas isso iria contrariar a filosofia enum strongmente tipada das enums com escopo definido do C ++ 11.

  2. Use um inteiro simples se você quiser armazenar uma combinação bitwise de enums:

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

    Mas isso reduziria tudo para um int , deixando você sem saber qual tipo você deveria colocar no método.

  3. Escreva uma classe separada que sobrecarregará os operadores e mantenha os sinalizadores bit a bit em um campo inteiro oculto:

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

    ( código completo user315052 )

    Mas você não tem nenhum IntelliSense ou qualquer suporte para sugerir os possíveis valores.

Eu sei que essa é uma pergunta subjetiva , mas: abordagem devo usar? Qual abordagem, se houver, é a mais amplamente reconhecida em C ++? Qual abordagem você usa ao lidar com campos de bits e por que ?

É claro que, desde que todas as três abordagens funcionem, estou procurando por razões factuais e técnicas, convenções geralmente aceitas e não simplesmente preferências pessoais.

Por exemplo, por causa do meu plano de fundo do C #, costumo ir com a abordagem 1 em C ++. Isso tem o benefício adicional de que meu ambiente de desenvolvimento pode me sugerir os valores possíveis, e com operadores de enumeração sobrecarregados isso é fácil de escrever e entender, e bastante limpo. E a assinatura do método mostra claramente que tipo de valor espera. Mas a maioria das pessoas aqui não concorda comigo, provavelmente por um bom motivo.

    
por Daniel Pelsmaeker 09.04.2013 / 13:11
fonte

6 respostas

25

A maneira mais simples é fornecer o operador sobrecarrega você mesmo. Estou pensando em criar uma macro para expandir as sobrecargas básicas por tipo.

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

(Observe que type_traits é um cabeçalho C ++ 11 e std::underlying_type_t é um recurso C ++ 14).

    
por 12.07.2013 / 01:29
fonte
6

Historicamente, eu sempre teria usado a enumeração antiga (pouco tipada) para nomear as constantes de bit, e apenas usava a classe de armazenamento explicitamente para armazenar o sinalizador resultante. Aqui, o ônus seria para garantir que minhas enumerações se encaixem no tipo de armazenamento e para acompanhar a associação entre o campo e suas constantes relacionadas.

Eu gosto da ideia de enums strongmente tipados, mas não estou muito confortável com a idéia de que variáveis de tipo enumerado podem conter valores que não estão entre as constantes dessa enumeração.

Por exemplo, assumindo o bit a bit ou foi sobrecarregado:

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

Para a sua terceira opção, você precisa de algum clichê para extrair o tipo de armazenamento da enumeração. Supondo que queremos forçar um tipo subjacente não assinado (podemos lidar com sinal também, com um pouco mais de código):

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;

Isso ainda não fornece IntelliSense ou autocompletion, mas a detecção do tipo de armazenamento é menos feia do que eu esperava originalmente.

Agora, eu encontrei uma alternativa: você pode especificar o tipo de armazenamento para uma enumeração de tipo fraco. Ele ainda tem a mesma sintaxe que em C #

enum E4 : int { ... };

Como ele é de tipagem fraca e converte implicitamente para / from int (ou qualquer tipo de armazenamento escolhido), parece menos estranho ter valores que não correspondam às constantes enumeradas.

A desvantagem é que isso é descrito como "transitório" ...

NB. essa variante adiciona suas constantes enumeradas ao escopo aninhado e fechado, mas você pode contornar isso com um namespace:

namespace E5 {
    enum Enum : int { A, B, C };
}
E5::Enum x = E5::A; // or E5::Enum::A
    
por 09.04.2013 / 15:35
fonte
3

Você pode definir sinalizadores enum com segurança de tipos no C ++ 11 usando std::enable_if . Esta é uma implementação rudimentar que pode estar faltando algumas coisas:

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

Observe que o number_of_bits infelizmente não pode ser preenchido pelo compilador, já que o C ++ não tem como fazer uma introspecção dos possíveis valores de uma enumeração.

Editar: Na verdade, eu estou corrigido, é possível obter o preenchimento do compilador number_of_bits para você.

Observe que isso pode manipular (descontroladamente ineficiente) um intervalo de valor enum não contínuo. Vamos apenas dizer que não é uma boa idéia usar o acima com um enum como este ou a loucura se seguirá:

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

Mas todas as coisas consideradas são uma solução bastante útil no final. Não precisa de bitfiddling do lado do usuário, é seguro quanto a tipos e dentro dos seus limites, por mais eficiente que seja (estou apoiando strongmente na std::bitset quality de implementação aqui ;) ).

    
por 20.12.2016 / 00:31
fonte
2

Eu odeio detesto macros no meu C ++ 14 tanto quanto o próximo, mas eu usei isso em todo lugar, e muito liberal também:

#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

Fazendo uso tão simples quanto

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

E, como dizem, a prova está no pudim:

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

Sinta-se livre para indefinir qualquer um dos operadores individuais como achar melhor, mas na minha opinião altamente tendenciosa, C / C ++ é para fazer interface com conceitos e fluxos de baixo nível, e você pode forçar esses operadores bit a , mãos mortas e eu vou lutar contra você com todas as macros profanas e feitiços lançadores de bits que posso conjurar para mantê-los.

    
por 26.08.2016 / 20:38
fonte
1

Um pequeno exemplo de enum-flags abaixo, parece muito com o C #.

Sobre a abordagem, na minha opinião: menos código, menos bugs, melhor código.

#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) é uma macro, definida em enum_flags.h (menos de 100 linhas, livre para usar sem restrições).

    
por 21.07.2013 / 12:42
fonte
0

Normalmente, você define um conjunto de valores inteiros que correspondem a números binários de conjuntos de bits únicos e, em seguida, adiciona-os. É assim que os programadores C geralmente fazem isso.

Então você teria (usando o operador bitshift para definir os valores, por exemplo, 1 < < 2 é o mesmo que o binário 100)

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

etc

Em C ++ você tem mais opções, defina um novo tipo em vez de um int (use typedef ) e similarmente definir valores como acima; ou definir um campo de bits ou um vetor de bools . Os dois últimos são muito eficientes em termos de espaço e fazem muito mais sentido em lidar com bandeiras. Um bitfield tem a vantagem de fornecer verificação de tipo (e, portanto, intellisense).

Eu diria (obviamente subjetivo) que um programador C ++ deveria usar um campo de bits para o seu problema, mas eu costumo ver a abordagem #define usada por programas C muito em programas C ++.

Suponho que o bitfield seja o mais próximo do enum do C #, porque o C # tentou sobrecarregar um enum para ser um tipo bitfield é estranho - um enum deve ser realmente um tipo "single-select".

    
por 09.04.2013 / 13:54
fonte