Uso de enumeraciones de ámbito para banderas de bits en C ++

60

Un enum X : int(C #) o enum class X : int(C ++ 11) es un tipo que tiene un campo interno oculto intque puede contener cualquier valor. Además, una serie de constantes predefinidas de Xse definen en la enumeración. Es posible convertir la enumeración a su valor entero y viceversa. Todo esto es cierto tanto en C # como en C ++ 11.

En C #, las enumeraciones no solo se utilizan para mantener valores individuales, sino también para mantener combinaciones de marcas en bits, según la recomendación de Microsoft . Tales enumeraciones están (generalmente, pero no necesariamente) decoradas con el [Flags]atributo. Para facilitar la vida de los desarrolladores, los operadores bit a bit (OR, AND, etc.) están sobrecargados para que pueda hacer fácilmente algo como esto (C #):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Soy un desarrollador experimentado de C #, pero he estado programando C ++ solo por un par de días y no conozco las convenciones de C ++. Tengo la intención de usar una enumeración de C ++ 11 de la misma manera que solía hacerlo en C #. En C ++ 11, los operadores bit a bit en las enumeraciones de ámbito no están sobrecargados, por lo que quería sobrecargarlos .

Esto solicitó un debate, y las opiniones parecen variar entre tres opciones:

  1. Una variable del tipo enum se usa para mantener el campo de bits, similar a 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)));

    Pero esto contrarrestaría la filosofía de enumeración fuertemente tipada de las enumeraciones de ámbito de C ++ 11.

  2. Use un entero simple si desea almacenar una combinación de enumeraciones bit a bit:

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

    Pero esto reduciría todo a una int, dejándote sin idea de qué tipo se supone que debes poner en el método.

  3. Escriba una clase separada que sobrecargue a los operadores y mantenga las banderas bit a bit en un campo entero 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 por usuario315052 )

    Pero entonces no tienes IntelliSense o cualquier soporte que te sugiera los posibles valores.

Sé que esta es una pregunta subjetiva , pero: ¿qué enfoque debo usar? ¿Qué enfoque, si lo hay, es el más ampliamente reconocido en C ++? ¿Qué enfoque utiliza cuando trata con campos de bits y por qué ?

Por supuesto, dado que los tres enfoques funcionan, busco razones fácticas y técnicas, convenciones generalmente aceptadas y no simplemente preferencias personales.

Por ejemplo, debido a mi experiencia en C #, tiendo a utilizar el enfoque 1 en C ++. Esto tiene el beneficio adicional de que mi entorno de desarrollo puede darme pistas sobre los posibles valores, y con operadores de enumeración sobrecargados, esto es fácil de escribir y comprender, y bastante limpio. Y la firma del método muestra claramente qué tipo de valor espera. Pero la mayoría de la gente aquí no está de acuerdo conmigo, probablemente por una buena razón.

Daniel AA Pelsmaeker
fuente
2
El comité ISO C ++ encontró la opción 1 lo suficientemente importante como para declarar explícitamente que el rango de valores de las enumeraciones incluye todas las combinaciones binarias de banderas. (Esto es anterior a C ++ 03) Así que hay una aprobación objetiva de esta pregunta algo subjetiva.
MSalters
1
(Para aclarar el comentario de @MSalters, el rango de una enumeración de C ++ se basa en su tipo subyacente (si es un tipo fijo), o de lo contrario en sus enumeradores. En este último caso, el rango se basa en el campo de bits más pequeño que puede contener todos los enumeradores definidos ; por ejemplo, para enum E { A = 1, B = 2, C = 4, };, el rango es 0..7(3 bits). Por lo tanto, el estándar C ++ garantiza explícitamente que # 1 siempre será una opción viable. [Específicamente, por enum classdefecto a enum class : intmenos que se especifique lo contrario, y por lo tanto siempre tiene un tipo subyacente fijo.])
Justin Time 2 Restablece a Monica el

Respuestas:

31

La forma más simple es proporcionarle al operador sobrecargas. Estoy pensando en crear una macro para expandir las 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;
}

(Tenga en cuenta que type_traitses un encabezado C ++ 11 y std::underlying_type_tes una característica de C ++ 14).

Dave
fuente
66
std :: subyacente_tipo_t es C ++ 14. Puede usar std :: subyacente_tipo <T> :: tipo en C ++ 11.
ddevienne
14
¿Por qué estás usando static_cast<T>para la entrada, pero el estilo C para el resultado aquí?
Ruslan
2
@Ruslan secundo esta pregunta
audiFanatic
¿Por qué te molestas con std :: subyacente_tipo_t cuando ya sabes que es int?
poizan42
1
Si SBJFrameDragse define en una clase y el |operador se usa más tarde en las definiciones de la misma clase, ¿cómo definiría el operador de modo que se pueda usar dentro de la clase?
HelloGoodbye
6

Históricamente, siempre habría usado la antigua enumeración (de tipo débil) para nombrar las constantes de bit, y solo había usado la clase de almacenamiento explícitamente para almacenar el indicador resultante. Aquí, la responsabilidad recaería en mí para asegurarme de que mis enumeraciones encajan en el tipo de almacenamiento y para realizar un seguimiento de la asociación entre el campo y sus constantes relacionadas.

Me gusta la idea de enumeraciones fuertemente tipadas, pero no estoy realmente cómodo con la idea de que las variables de tipo enumerado pueden contener valores que no están entre las constantes de esa enumeración.

Por ejemplo, suponiendo que el bit a bit o se ha sobrecargado:

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 su tercera opción, necesita algo repetitivo para extraer el tipo de almacenamiento de la enumeración. Asumiendo que queremos forzar un tipo subyacente sin signo (también podemos manejar con signo, con un poco más 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;

Esto todavía no le da IntelliSense o autocompletado, pero la detección del tipo de almacenamiento es menos fea de lo que originalmente esperaba.


Ahora, encontré una alternativa: puede especificar el tipo de almacenamiento para una enumeración de tipo débil. Incluso tiene la misma sintaxis que en C #

enum E4 : int { ... };

Debido a que tiene un tipo débil y se convierte implícitamente a / desde int (o cualquier tipo de almacenamiento que elija), se siente menos extraño tener valores que no coincidan con las constantes enumeradas.

La desventaja es que esto se describe como "transicional" ...

NÓTESE BIEN. Esta variante agrega sus constantes enumeradas tanto al ámbito anidado como al delimitador, pero puede solucionar esto con un espacio de nombres:

namespace E5 {
    enum Enum : int { A, B, C };
}
E5::Enum x = E5::A; // or E5::Enum::A
Inútil
fuente
1
Otra desventaja de las enumeraciones débilmente escritas es que sus constantes contaminan mi espacio de nombres, ya que no necesitan tener el prefijo con el nombre de la enumeración. Y eso también puede causar todo tipo de comportamiento extraño si tiene dos enumeraciones diferentes, ambas con un miembro con el mismo nombre.
Daniel AA Pelsmaeker
Es verdad. La variante de tipo débil con el tipo de almacenamiento especificado agrega sus constantes tanto al alcance envolvente como a su propio alcance, iiuc.
Inútil el
El enumerador sin ámbito solo se declara en el ámbito circundante. Poder calificarlo con el nombre de enumeración es parte de las reglas de búsqueda, no la declaración. C ++ 11 7.2 / 10: cada nombre de enumeración y cada enumerador sin ámbito se declara en el ámbito que contiene inmediatamente el especificador de enumeración. Cada enumerador de ámbito se declara en el alcance de la enumeración. Estos nombres obedecen las reglas de alcance definidas para todos los nombres en (3.3) y (3.4).
Lars Viklund
1
con C ++ 11 tenemos std :: subyacente_tipo que proporciona el tipo subyacente de una enumeración. Entonces tenemos 'template <typename IntegralType> struct Integral {typedef typename std :: subyacente_tipo <IntegralType> :: type Type; }; `En C ++ 14, esto se simplifica aún más para 'plantilla <typename IntegralType> struct Integral {typedef std :: subyacente_tipo_t <IntegralType> Tipo; };
Emsr
4

Puede definir indicadores de enumeración de tipo seguro en C ++ 11 utilizando std::enable_if. Esta es una implementación rudimentaria que puede faltar algunas cosas:

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

Tenga en cuenta number_of_bitsque desafortunadamente el compilador no puede completarlo, ya que C ++ no tiene ninguna forma de hacer una introspección de los posibles valores de una enumeración.

Editar: En realidad estoy corregido, es posible que el compilador lo complete number_of_bitspor usted.

Tenga en cuenta que esto puede manejar (muy poco eficientemente) un rango de valores de enumeración no continuo. Digamos que no es una buena idea usar lo anterior con una enumeración como esta o se producirá locura:

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

Pero a fin de cuentas, al final es una solución bastante útil. No necesita ningún bitfiddling del lado del usuario, es de tipo seguro y dentro de sus límites, tan eficiente como sea posible (me estoy inclinando fuertemente por la std::bitsetcalidad de implementación aquí ;)).

rubenvb
fuente
Estoy seguro de que me perdí algunas sobrecargas de los operadores.
rubenvb
2

yo odio Detesto las macros en mi C ++ 14 tanto como el siguiente chico, pero he empezado a usar esto por todas partes, y también bastante liberalmente:

#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

Hacer uso tan simple como

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

Y, como dicen, la prueba está en el budín:

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

Siéntase libre de definir cualquiera de los operadores individuales como mejor le parezca, pero en mi opinión altamente sesgada, C / C ++ es para interactuar con conceptos y flujos de bajo nivel, y puede sacar estos operadores bit a bit de mis manos frías y muertas. y lucharé contra ti con todas las macros impías y hechizos que puedo conjurar para mantenerlos.

Mahmoud Al-Qudsi
fuente
2
Si detesta tanto las macros, ¿por qué no usar una construcción C ++ adecuada y escribir algunos operadores de plantilla en lugar de las macros? Podría decirse que el enfoque de la plantilla es mejor porque se puede utilizar std::enable_ifcon std::is_enumrestringir sus sobrecargas de operadores en abierto sólo se trabaja con tipos enumerados. También he agregado operadores de comparación (usando std::underlying_type) y el operador lógico no para cerrar aún más la brecha sin perder el tipeo fuerte. La única cosa que no puedo coincidir es la conversión implícita a bool, pero flags != 0y !flagsson suficientes para mí.
monkey0506
1

Por lo general, definiría un conjunto de valores enteros que corresponden a números binarios de conjunto de un solo bit, luego los sumaría. Esta es la forma en que los programadores de C generalmente lo hacen.

Entonces debería (usando el operador de desplazamiento de bits para establecer los valores, por ejemplo, 1 << 2 es lo mismo que binario 100)

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

etc.

En C ++ tiene más opciones, defina un nuevo tipo en lugar de int (use typedef ) y establezca valores similares a los anteriores; o definir un campo de bits o un vector de bools . Los últimos 2 son muy eficientes en cuanto al espacio y tienen mucho más sentido para lidiar con las banderas. Un campo de bits tiene la ventaja de proporcionarle verificación de tipos (y, por lo tanto, intellisense).

Yo diría (obviamente subjetivo) que un programador de C ++ debería usar un campo de bits para su problema, pero tiendo a ver mucho el enfoque #define utilizado por los programas de C en los programas de C ++.

Supongo que el campo de bits es el más cercano a la enumeración de C #, por qué C # intentó sobrecargar una enumeración para que sea un tipo de campo de bits es extraño: una enumeración realmente debería ser un tipo de "selección única".

gbjbaanb
fuente
11
usar macros en c ++ de esa manera es malo
Bћовић
3
C ++ 14 le permite definir literales binarios (por ejemplo 0b0100) para que el 1 << nformato sea obsoleto.
Rob K
Tal vez quisiste decir bitset en lugar de bitfield .
Jorge Bellon
1

Un breve ejemplo de enum-flags a continuación, se parece bastante a C #.

Sobre el enfoque, en mi opinión: menos código, menos errores, mejor 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) es una macro, definida en enum_flags.h (menos de 100 líneas, de uso gratuito sin restricciones).

Yuri Yaryshev
fuente
1
¿es el archivo enum_flags.h el mismo que en la primera revisión de su pregunta? en caso afirmativo, puede usar la URL de revisión para referirse a ella: http://programmers.stackexchange.com/revisions/205567/1
gnat
+1 se ve bien, limpio. Probaré esto en nuestro proyecto SDK.
Garet Claborn
1
@GaretClaborn Esto es lo que yo llamaría clean: paste.ubuntu.com/23883996
sehe
1
Por supuesto, perdí el ::typeallí. Corregido: paste.ubuntu.com/23884820
sehe
@sehe hey, no se supone que el código de la plantilla sea legible y tenga sentido. ¿Qué es esta brujería? bonito ... ¿este fragmento está abierto para usar jaja?
Garet Claborn
0

Hay otra forma de desollar al gato:

En lugar de sobrecargar los operadores de bits, al menos algunos prefieren simplemente agregar un revestimiento de 4 para ayudarlo a evitar esa desagradable restricción de enumeraciones de ámbito:

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

Por supuesto, debe escribir la ut_cast()cosa cada vez, pero en el lado positivo, esto produce un código más legible, en el mismo sentido que el uso static_cast<>(), en comparación con la conversión de tipo implícito o operator uint16_t()tipo de cosas.

Y seamos honestos aquí, usar el tipo Foocomo en el código anterior tiene sus peligros:

En otro lugar, alguien podría hacer un cambio de mayúscula a variable fooy no esperar que tenga más de un valor ...

Por lo tanto, ensuciar el código ut_cast()ayuda a alertar a los lectores de que algo extraño está sucediendo.

BitTickler
fuente