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


60

Um enum X : int(C #) ou enum class X : int(C ++ 11) é um tipo que possui um campo interno oculto intque pode conter qualquer valor. Além disso, várias constantes predefinidas de Xsão definidas na enumeração. É possível converter a enumeração em seu valor inteiro e vice-versa. Isso tudo é verdade em C # e C ++ 11.

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

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Eu sou um desenvolvedor experiente de C #, mas tenho programado C ++ apenas há alguns dias e não sou conhecido pelas convenções de C ++. Pretendo usar uma enumeração C ++ 11 da mesma maneira que costumava fazer em C #. No C ++ 11, os operadores bit a bit em enumerações com escopo não sã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 contraria a filosofia de enumeração fortemente tipada das enumerações com escopo definido em C ++ 11.

  2. Use um número inteiro simples se desejar armazenar uma combinação de enumerações bit a bit:

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

    Mas isso reduziria tudo a um int, deixando você sem idéia de 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 do usuário315052 )

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

Sei que essa é uma pergunta subjetiva , mas: que 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 quê ?

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

Por exemplo, devido ao meu background em C #, eu costumo seguir 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 sobrecarregados de enum, 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 discorda de mim, provavelmente por um bom motivo.


2
O comitê ISO C ++ considerou a opção 1 importante o suficiente para declarar explicitamente que o intervalo de valores de enumerações inclui todas as combinações binárias de sinalizadores. (Isso é anterior ao C ++ 03) Portanto, há uma aprovação objetiva dessa questão um tanto subjetiva.
MSalters

11
(Para esclarecer o comentário do @MSalters, o intervalo de uma enumeração C ++ é baseado em seu tipo subjacente (se for um tipo fixo) ou em seus enumeradores. No último caso, o intervalo é baseado no menor campo de bits que pode conter todos os enumeradores definidos ; por exemplo, para enum E { A = 1, B = 2, C = 4, };, o intervalo é 0..7(3 bits). Portanto, o padrão C ++ garante explicitamente que o número 1 sempre será uma opção viável. [Especificamente, o enum classpadrão é aquele a enum class : intmenos que seja especificado de outra forma e, portanto, sempre tem um tipo subjacente fixo.])
Justin Time 2 Restabelecer Monica

Respostas:


31

A maneira mais simples é fornecer que o operador se sobrecarregue. 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.)


6
std :: subjac_type_t é C ++ 14. Pode usar std :: subjac_type <T> :: type no C ++ 11.
precisa saber é o seguinte

14
Por que você está usando static_cast<T>a entrada, mas a conversão no estilo C para o resultado aqui?
Ruslan

2
@Ruslan Eu segunda esta pergunta
audiFanatic 8/16

Por que você está se incomodando com std :: subjac_type_t quando você já sabe que é int?
poizan42

11
Se SBJFrameDragfor definido em uma classe e o |operador for usado posteriormente nas definições da mesma classe, como você definiria o operador para que ele pudesse ser usado dentro da classe?
hellogoodbye

6

Historicamente, eu sempre teria usado a enumeração antiga (de tipo fraco) para nomear as constantes de bits e apenas usado a classe de armazenamento explicitamente para armazenar o sinalizador resultante. Aqui, o ônus dependeria de mim para garantir que minhas enumerações se encaixassem no tipo de armazenamento e para acompanhar a associação entre o campo e as constantes relacionadas.

Gosto da ideia de enumerações fortemente tipadas, mas não me sinto muito à vontade com a ideia de que variáveis ​​do 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 sua terceira opção, você precisa de um padrão para extrair o tipo de armazenamento da enumeração. Supondo que queremos forçar um tipo subjacente não assinado (também podemos manipular assinados, 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 oferece o IntelliSense ou o preenchimento automático, mas a detecção do tipo de armazenamento é menos feia do que eu esperava inicialmente.


Agora, 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 é de tipo fraco e converte implicitamente para / de int (ou qualquer outro tipo de armazenamento que você escolher), é menos estranho ter valores que não correspondem às constantes enumeradas.

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

NB essa variante adiciona suas constantes enumeradas ao escopo aninhado e ao anexo, mas você pode contornar isso com um espaço para nome:

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

11
Outra desvantagem das enumerações fracamente digitadas é que suas constantes poluem meu espaço para nome, pois elas não precisam ser prefixadas com o nome da enumeração. E isso também pode causar todo tipo de comportamento estranho se você tiver duas enums diferentes, ambas com um membro com o mesmo nome.
Daniel Daniel Pelsmaeker

Isso é verdade. A variante de tipo fraco com o tipo de armazenamento especificado adiciona suas constantes ao escopo anexo e ao seu próprio escopo, iiuc.
inútil

O enumerador sem escopo é declarado apenas no escopo circundante. Ser capaz de qualificá-lo pelo enum-name faz parte das regras de pesquisa, não da declaração. C ++ 11 7.2 / 10: Cada nome de enumeração e cada enumerador sem escopo é declarado no escopo que contém imediatamente o especificador de enumeração. Cada enumerador com escopo definido é declarado no escopo da enumeração. Esses nomes obedecem às regras de escopo definidas para todos os nomes em (3.3) e (3.4).
Lars Viklund

11
com C ++ 11, temos std :: subjac_type que fornece o tipo subjacente de uma enumeração. Portanto, temos 'template <typename IntegralType> struct Integral {typedef typename std :: subjacente_tipo <IntegralType> :: type Type; }; `No C ++ 14, isso é ainda mais simplificado para modelar <typename IntegralType> struct Integral {typedef std :: subjacente_tipo_t <IntegralType> Type; };
Emsr

4

Você pode definir sinalizadores de enumeração com segurança de tipo 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, number_of_bitsinfelizmente, não pode ser preenchido pelo compilador, pois o C ++ não tem como fazer uma introspecção dos possíveis valores de uma enumeração.

Edit: Na verdade, eu estou corrigido, é possível obter o preenchimento do compilador number_of_bitspara você.

Observe que isso pode manipular (de maneira ineficiente) um intervalo de valores de enum não contínuo. Digamos apenas que não é uma boa ideia usar o item acima com uma enumeração como esta ou com a loucura:

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 nenhuma manipulação de bits do lado do usuário, é seguro para o tipo e dentro de seus limites, o mais eficiente possível (estou me baseando fortemente na std::bitsetqualidade da implementação aqui ;)).


Tenho certeza de que perdi algumas sobrecargas dos operadores.
amigos estão dizendo sobre rubenvb

2

Eu ódio detesto macros no meu C ++ 14 tanto quanto o próximo, mas passei a usá-lo em todo o lugar, e de maneira bastante 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 se costuma dizer, 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 definir qualquer um dos operadores individuais como achar melhor, mas, na minha opinião altamente tendenciosa, o C / C ++ é para interface com conceitos e fluxos de baixo nível, e você pode retirar esses operadores bit a bit das minhas mãos frias e mortas e lutarei com você com todas as macros profanas e feitiços que eu posso invocar para mantê-las.


2
Se você detesta tanto as macros, por que não usar uma construção C ++ adequada e escrever alguns operadores de modelo em vez das macros? Indiscutivelmente, a abordagem do modelo é melhor porque você pode usar std::enable_ifcom std::is_enumpara restringir as sobrecargas gratuitas do operador a trabalhar apenas com tipos enumerados. Também adicionei operadores de comparação (usando std::underlying_type) e o operador não lógico para preencher ainda mais a lacuna sem perder a digitação forte. A única coisa que não pode corresponder é a conversão implícita para bool, mas flags != 0e !flagssão suficientes para mim.
monkey0506

1

Normalmente, você define um conjunto de valores inteiros que correspondem aos números binários de um bit e os adiciona. É 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 100 binário)

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

etc

No C ++, você tem mais opções, defina um novo tipo em vez de int (use typedef ) e defina valores semelhantes, como acima; ou defina 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 para lidar com sinalizadores. Um campo de bits tem a vantagem de fornecer verificação de tipo (e, portanto, intellisense).

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

Suponho que o campo de bits seja o mais próximo do enum do C #, por que o C # tentou sobrecarregar um enum para ser do tipo de campo de bits é estranho - um enum deve realmente ser do tipo "seleção única".


11
usando macros em C ++ de tal maneira é ruim
BЈовић

3
O C ++ 14 permite definir literais binários (por exemplo 0b0100), para que o 1 << nformato fique meio obsoleto.
Rob K

Talvez você quis dizer bitset em vez de bitfield.
Jorge Bellon

1

Um pequeno exemplo de enum-flags abaixo, se 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).


11
o arquivo enum_flags.h é o mesmo da 1ª revisão da sua pergunta? se sim, você pode usar o URL de revisão para consultá-lo: http://programmers.stackexchange.com/revisions/205567/1
gnat

+1 parece bom, limpo. Vou tentar isso em nosso projeto SDK.
Garet Claborn

11
@GaretClaborn Isto é o que eu chamaria de limpo: paste.ubuntu.com/23883996
sehe

11
Claro, perdi o ::typelá. Corrigido: paste.ubuntu.com/23884820
veja

@ hehe hey, o código do modelo não deve ser legível e fazer sentido. o que é essa bruxaria? bom .... é esse trecho aberto ao uso lol
Garet Claborn

0

Existe ainda outra maneira de esfolar o gato:

Em vez de sobrecarregar os operadores de bit, pelo menos alguns podem preferir adicionar um liner 4 para ajudá-lo a contornar essa restrição desagradável de enumerações com escopo definido:

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

É verdade que você precisa digitar a ut_cast()coisa toda vez, mas, no lado superior, isso gera um código mais legível, no mesmo sentido que o uso static_cast<>(), comparado à conversão implícita de tipos ou operator uint16_t()coisas do tipo.

E vamos ser honestos aqui, usar type Foocomo no código acima tem seus perigos:

Em outro lugar, alguém pode alternar entre variáveis fooe não esperar que ele tenha mais de um valor ...

Assim, colocar o código de lado ut_cast()ajuda a alertar os leitores de que algo suspeito está acontecendo.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.