Uso de enumeraciones de ámbito para indicadores de bit en C ++

53

Un enum X : int (C #) o enum class X : int (C ++ 11) es un tipo que tiene un campo interno oculto de int que puede contener cualquier valor. Además, una serie de constantes predefinidas de X se 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 usan para mantener valores individuales, sino también para mantener combinaciones de indicadores a nivel de bits, según recomendación de Microsoft . Tales enumeraciones están (generalmente, pero no necesariamente) decoradas con el atributo [Flags] . Para facilitar la vida de los desarrolladores, los operadores bitwise (OR, AND, etc ...) están sobrecargados para que pueda hacer algo como esto (C #):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Soy un desarrollador de C # experimentado, pero he estado programando C ++ solo por un par de días, y no soy conocido con las convenciones de C ++. Tengo la intención de usar una enumeración de C ++ 11 exactamente de la misma manera que solía hacerlo en C #. En C ++ 11, los operadores bitwise en enumerados 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 de enumeración 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 con alcance de C ++ 11.

  2. Use un entero liso si desea almacenar una combinación de enumeraciones a nivel de bits:

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

    Pero esto reduciría todo a un int , dejándote sin una pista sobre 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 bitwise 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 user315052 )

    Pero entonces no tiene IntelliSense o cualquier otro soporte para sugerirle los posibles valores.

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

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

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

    
pregunta Daniel Pelsmaeker 09.04.2013 - 13:11

6 respuestas

25

La forma más sencilla es proporcionar sobrecargas al operador usted mismo. 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_traits es un encabezado de C ++ 11 y std::underlying_type_t es una característica de C ++ 14)

    
respondido por el Dave 12.07.2013 - 01:29
6

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

Me gusta la idea de enums fuertemente tipados, 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 ha sido 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 de repetitivo para extraer el tipo de almacenamiento de la enumeración. Suponiendo que deseamos forzar un tipo subyacente sin firmar (también podemos controlarlo 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 proporciona IntelliSense o autocompletar, 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 está mal escrito y se convierte implícitamente a / desde int (o cualquier tipo de almacenamiento que elija), parece menos extraño tener valores que no coincidan con las constantes enumeradas.

El inconveniente es que esto se describe como "transicional" ...

NB. esta variante agrega sus constantes enumeradas tanto al ámbito anidado como al adjunto, 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
    
respondido por el Useless 09.04.2013 - 15:35
3

Puede definir indicadores de enumeración seguros para el tipo en C ++ 11 usando std::enable_if . Esta es una implementación rudimentaria que puede faltar en 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 que, por desgracia, el compilador no puede completar number_of_bits , ya que C ++ no tiene forma de hacer una introspección de los posibles valores de una enumeración.

Editar: De hecho, estoy corregido, es posible que el compilador llene number_of_bits para usted.

Tenga en cuenta que esto puede manejar (de manera ineficiente) 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 la locura se producirá:

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

Pero a fin de cuentas, esta es una solución muy útil al final. No necesita ningún bitfiddling del lado del usuario, es de tipo seguro y dentro de sus límites, tan eficiente como se obtiene (me inclino fuertemente por la calidad de implementación std::bitset aquí ;) ).

    
respondido por el rubenvb 20.12.2016 - 00:31
2

Yo odio detesto las macros en mi C ++ 14 tanto como el siguiente tipo, pero he empezado a usar esto en todo el lugar, y muy generosamente también:

#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

Haciendo uso tan simple como

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

Y, como dicen, la prueba está en el pudí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 anular la definición de cualquiera de los operadores individuales como lo considere oportuno, pero en mi opinión muy sesgada, C / C ++ es para interactuar con conceptos y flujos de bajo nivel, y puede sacar a estos operadores de bitwise de mi resfriado , manos muertas y te lucharé con todas las macros profanas y hechizos que puedo invocar para mantenerlos.

    
respondido por el Mahmoud Al-Qudsi 26.08.2016 - 20:38
1

Un breve ejemplo de los indicadores de enumeración 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).

    
respondido por el Yuri Yaryshev 21.07.2013 - 12:42
0

Por lo general, definiría un conjunto de valores enteros que se corresponden con los números binarios de un solo bit y luego los agregará. Esta es la forma en que los programadores de C usualmente lo hacen.

Entonces tendrías (usando el operador de cambio 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 un int (use typedef ) y establecer valores similares a los anteriores; o defina un campo de bits o un vector of bools . Los 2 últimos son muy eficientes en cuanto al espacio y tienen mucho más sentido para tratar con banderas. Un campo de bits tiene la ventaja de proporcionarle una comprobación de tipo (y, por lo tanto, inteligencia).

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 lo que C # intentó sobrecargar una enumeración para que sea un tipo de campo de bits es extraño, una enumeración debería ser un tipo de "selección única".

    
respondido por el gbjbaanb 09.04.2013 - 13:54

Lea otras preguntas en las etiquetas