c++ - En busca de una mejor enumeración bitflag
enums bit-manipulation (4)
Bien, estamos en C ++ 17 y todavía no hay una respuesta satisfactoria para una interfaz de bitflags realmente buena en C ++.
Tenemos una enum
que sangra sus valores de miembro en el ámbito de inclusión, pero se convierte implícitamente a su tipo subyacente, por lo que se pueden usar como si fueran indicadores de bits pero se niegan a reasignar nuevamente a la enumeración sin lanzar.
Tenemos una enum class
que resuelve el problema del alcance del nombre, por lo que sus valores deben denominarse explícitamente MyEnum::MyFlag
o incluso MyClass::MyEnum::MyFlag
, pero no se convierten de forma implícita a su tipo subyacente, por lo que no se pueden usar como bit Banderas sin colada interminable de ida y vuelta.
Y finalmente, tenemos los antiguos campos de bits de C
como:
struct FileFlags {
unsigned ReadOnly : 1;
unsigned Hidden : 1;
...
};
Lo que tiene el inconveniente de no tener una buena forma de inicializarse como un todo: uno tiene que recurrir al uso de memset o la conversión de la dirección o similar para sobrescribir todo el valor o inicializarlo todo de una vez o manipular múltiples bits a la vez. También sufre de no poder nombrar el valor de un indicador dado, a diferencia de su dirección, por lo que no hay un nombre que represente 0x02, mientras que hay un nombre así cuando se usan enumeraciones, por lo tanto, es fácil con las enumeraciones nombrar una combinación de banderas, como FileFlags::ReadOnly | FileFlags::Hidden
FileFlags::ReadOnly | FileFlags::Hidden
: simplemente no hay una buena manera de decir tanto para los campos de bits.
Además, todavía tenemos simples constexpr
o #define
para nombrar valores de bits y luego simplemente no utilizamos enumeraciones. Esto funciona, pero disocia completamente los valores de bit del tipo de bitflag subyacente. ¿Quizás este último no sea el peor enfoque, particularmente si los valores de constexpr
bit están constexpr
dentro de una estructura para darles su propio nombre-alcance?
struct FileFlags {
constexpr static uint16_t ReadOnly = 0x01u;
constexpr static uint16_t Hidden = 0x02u;
...
}
Así que, en la actualidad, tenemos muchas técnicas, ninguna de las cuales se suma a una forma realmente sólida de decir
Aquí hay un tipo que tiene los siguientes indicadores de bits válidos en él, tiene su propio ámbito de nombre, y estos bits y tipo deberían poder utilizarse libremente con operadores estándar a nivel de bits, como | & ^ ~, y deben ser comparables a valores integrales como 0, y el resultado de cualquier operador a nivel de bits debe seguir siendo el tipo nombrado, y no convertirse en una integral
Dicho todo esto, hay una serie de intentos para tratar de producir la entidad anterior en C ++:
- El equipo del sistema operativo Windows desarrolló una macro simple que genera el código C ++ para definir los operadores que faltan en un tipo de enumeración determinado
DEFINE_ENUM_FLAG_OPERATORS(EnumType)
que luego define el operador | & ^ ~ y las operaciones de asignación asociadas como | = y etc. - ''grisumbras'' tiene un proyecto GIT público para habilitar la semántica de bitflag con enumeraciones de ámbito here , que utiliza la programación meta de
enable_if
para permitir que una enumeración determinada se convierta en un tipo deenable_if
bit que admita a los operadores que faltan y vuelva de nuevo en silencio. - Sin saber lo anterior, escribí un contenedor de bit_flags relativamente simple que define todos los operadores bitwise en sí mismo, de modo que uno puede usar las
bit_flags<EnumType> flags
y luego lasflags
tiene una semántica bitwise. Lo que no puede hacer es permitir que la base enumerada maneje los operadores bitwise directamente, por lo que no puede decirEnumType::ReadOnly | EnumType::Hidden
EnumType::ReadOnly | EnumType::Hidden
incluso cuando se usa unbit_flags<EnumType>
porque la enumeración subyacente en sí misma no es compatible con los operadores necesarios. Tuve que terminar haciendo lo mismo esencialmente como los # 1 y # 2 anteriores, y habilitar aloperator | (EnumType, EnumType)
operator | (EnumType, EnumType)
para los diversos operadores bitwise que requieren que los usuarios declaren una especialización para un meta tipo para su enum, comotemplate <> struct is_bitflag_enum<EnumType> : std::true_type {};
En última instancia, el problema con # 1, # 2 y # 3 es que no es posible (que yo sepa) definir los operadores que faltan en la enumeración (como en # 1) o definir el tipo de habilitador necesario ( por ejemplo, template <> struct is_bitflag_enum<EnumType> : std::true_type {};
como en # 2 y parcialmente # 3) en el alcance de la clase. Deben ocurrir fuera de una clase o estructura, ya que C ++ simplemente no tiene un mecanismo que yo sepa que me permitiría hacer tales declaraciones dentro de una clase.
Así que ahora, tengo el deseo de tener un conjunto de indicadores que deben estar incluidos en una clase dada, pero no puedo usar esos indicadores dentro del encabezado de la clase (por ejemplo, inicialización predeterminada, funciones en línea, etc.) porque no puedo habilitar ninguno de los maquinaria que permite tratar la enumeración como bitflags hasta después de la llave de cierre para la definición de clase. O bien, puedo definir todas las enumeraciones de bandera fuera de la clase a la que pertenecen, de modo que luego pueda invocar el "convertir esta enumeración en un tipo bit a bit" antes de la definición de la clase de usuario, para tener un uso completo de esa funcionalidad en la clase cliente, pero ahora las banderas de bits están en el ámbito externo en lugar de estar asociadas a la clase en sí.
Este no es el fin del mundo, ninguno de los anteriores lo es. Pero todo esto causa un sinfín de dolores de cabeza al escribir mi código, y me impide escribirlo de la manera más natural, es decir, con un enumeración de bandera dada que pertenece a una clase específica dentro de la clase de cliente (con ámbito), pero con una bandera a nivel de bits -semantics (mi enfoque # 3 casi permite esto, siempre que todo esté envuelto por un bit_flags - para habilitar explícitamente la compatibilidad bitwise necesaria).
¡Todo esto todavía me deja con la molesta sensación de que esto podría ser mucho mejor de lo que es!
Seguramente debería haber, y quizás lo haya hecho, pero aún no lo he descubierto, un enfoque a las enumeraciones para habilitar a los operadores bitwise en ellos, al tiempo que permite que se declaren y utilicen dentro de un ámbito de clase adjunto ...
¿Alguien tiene una toallita o un enfoque que no haya considerado anteriormente, que me permita "el mejor de todos los mundos posibles" en esto?
Implementar un conjunto de bits propio con una selección de tipo entero subyacente no es difícil. El problema con la enumeración es que falta la metainformación necesaria para adaptarse al conjunto de bits. Pero aún con la programación adecuada de metadatos y los rasgos de habilitación de marca es posible tener tal sintaxis:
flagset<file_access_enum> rw = bit(read_access_flag)|bit(write_access_flag);
Por ejemplo
// union only for convenient bit access.
typedef union a
{ // it has its own name-scope
struct b
{
unsigned b0 : 1;
unsigned b2 : 1;
unsigned b3 : 1;
unsigned b4 : 1;
unsigned b5 : 1;
unsigned b6 : 1;
unsigned b7 : 1;
unsigned b8 : 1;
//...
} bits;
unsigned u_bits;
// has the following valid bit-flags in it
typedef enum {
Empty = 0u,
ReadOnly = 0x01u,
Hidden = 0x02u
} Values;
Values operator =(Values _v) { u_bits = _v; return _v; }
// should be freely usable with standard bitwise operators such as | & ^ ~
union a& operator |( Values _v) { u_bits |= _v; return *this; }
union a& operator &( Values _v) { u_bits &= _v; return *this; }
union a& operator |=( Values _v) { u_bits |= _v; return *this; }
union a& operator &=( Values _v) { u_bits &= _v; return *this; }
// ....
// they should be comparable to integral values such as 0
bool operator <( unsigned _v) { return u_bits < _v; }
bool operator >( unsigned _v) { return u_bits > _v; }
bool operator ==( unsigned _v) { return u_bits == _v; }
bool operator !=( unsigned _v) { return u_bits != _v; }
} BITS;
int main()
{
BITS bits;
int integral = 0;
bits = bits.Empty;
// they should be comparable to integral values such as 0
if ( bits == 0)
{
bits = bits.Hidden;
// should be freely usable with standard bitwise operators such as | & ^ ~
bits = bits | bits.ReadOnly;
bits |= bits.Hidden;
// the result of any bitwise operators should remain the named type, and not devolve into an integral
//bits = integral & bits; // error
//bits |= integral; // error
}
}
Yo uso la enum class
con los siguientes operadores de plantillas:
template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM operator |( ENUM lhs, ENUM rhs )
{
return static_cast< ENUM >( static_cast< UInt32 >( lhs ) | static_cast< UInt32 >( rhs ));
}
template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator |=( ENUM& lhs, ENUM rhs )
{
lhs = lhs | rhs;
return lhs;
}
template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline UInt32 operator &( ENUM lhs, ENUM rhs )
{
return static_cast< UInt32 >( lhs ) & static_cast< UInt32 >( rhs );
}
template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator &=( ENUM& lhs, ENUM rhs )
{
lhs = lhs & rhs;
return lhs;
}
template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator &=( ENUM& lhs, int rhs )
{
lhs = static_cast< ENUM >( static_cast< int >( lhs ) & rhs );
return lhs;
}
Si le preocupan los operadores anteriores que se filtran a otras enumeraciones, supongo que podría encapsularlos en el mismo espacio de nombres donde se declara la enumeración, o incluso simplemente implementarlos enumeración por enumeración (solía usar una macro para ese). En general, sin embargo, he considerado ese exceso y ahora los he declarado dentro de mi espacio de nombres de nivel superior para que cualquier código los use.
Adopto el enfoque de FlagSet de FlagSet
en Code Review SE .
La clave es introducir un nuevo tipo que sirva como "contenedor" para uno o más valores activados desde una lista fija de opciones. Dicho contenedor es un envoltorio alrededor de un conjunto de bitset
que toma, como entrada, instancias de una enumeración de ámbito.
Es de tipo seguro gracias a la enumeración del ámbito y puede realizar operaciones de tipo bit a través de la sobrecarga del operador, delegando en operaciones de bits. Y aún puede usar la enumeración de alcance directamente si lo desea, y si no necesita las operaciones a nivel de bits o almacenar múltiples indicadores.
Para la producción, hice algunos cambios en el código vinculado; Algunos de ellos se discuten en los comentarios en la página de Revisión de Código.