Pregunta

En un proyecto de C++ en el que estoy trabajando, tengo un bandera tipo de valor que puede tener cuatro valores.Esas cuatro banderas se pueden combinar.Las banderas describen los registros en la base de datos y pueden ser:

  • nuevo record
  • registro eliminado
  • registro modificado
  • registro existente

Ahora, para cada registro deseo mantener este atributo, así que podría usar una enumeración:

enum { xNew, xDeleted, xModified, xExisting }

Sin embargo, en otros lugares del código, necesito seleccionar qué registros serán visibles para el usuario, por lo que me gustaría poder pasarlo como un único parámetro, como:

showRecords(xNew | xDeleted);

Entonces, parece que tengo tres enfoques posibles:

#define X_NEW      0x01
#define X_DELETED  0x02
#define X_MODIFIED 0x04
#define X_EXISTING 0x08

o

typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;

o

namespace RecordType {
    static const uint8 xNew = 1;
    static const uint8 xDeleted = 2;
    static const uint8 xModified = 4;
    static const uint8 xExisting = 8;
}

Los requisitos de espacio son importantes (byte vs int) pero no cruciales.Con define pierdo seguridad de tipos, y con enum Pierdo algo de espacio (enteros) y probablemente tenga que emitir cuando quiero realizar una operación bit a bit.Con const Creo que también pierdo la seguridad de tipos desde un azar uint8 podría entrar por error.

¿Existe alguna otra forma más limpia?

Si no, ¿qué usarías y por qué?

PDEl resto del código es C++ bastante limpio y moderno sin #defines, y he usado espacios de nombres y plantillas en algunos espacios, por lo que tampoco están fuera de discusión.

¿Fue útil?

Solución

Combinar las estrategias para reducir las desventajas de un solo enfoque.Trabajo en sistemas integrados, por lo que la siguiente solución se basa en el hecho de que los operadores enteros y bit a bit son rápidos, tienen poca memoria y utilizan poco flash.

Coloque la enumeración en un espacio de nombres para evitar que las constantes contaminen el espacio de nombres global.

namespace RecordType {

Una enumeración declara y define un tiempo de compilación marcado con tipo.Utilice siempre la verificación de tipos en tiempo de compilación para asegurarse de que los argumentos y las variables reciban el tipo correcto.No es necesario el typedef en C++.

enum TRecordType { xNew = 1, xDeleted = 2, xModified = 4, xExisting = 8,

Cree otro miembro para un estado no válido.Esto puede resultar útil como código de error;por ejemplo, cuando desea devolver el estado pero la operación de E/S falla.También es útil para depurar;Úselo en listas de inicialización y destructores para saber si se debe usar el valor de la variable.

xInvalid = 16 };

Considera que tienes dos propósitos para este tipo.Para rastrear el estado actual de un registro y crear una máscara para seleccionar registros en ciertos estados.Cree una función en línea para probar si el valor del tipo es válido para su propósito;como marcador de estado frente a máscara de estado.Esto detectará errores a medida que typedef es solo un int y un valor como 0xDEADBEEF puede estar en su variable a través de variables no inicializadas o erróneas.

inline bool IsValidState( TRecordType v) {
    switch(v) { case xNew: case xDeleted: case xModified: case xExisting: return true; }
    return false;
}

 inline bool IsValidMask( TRecordType v) {
    return v >= xNew  && v < xInvalid ;
}

Agrega un using directiva si desea utilizar el tipo con frecuencia.

using RecordType ::TRecordType ;

Las funciones de verificación de valores son útiles en afirmaciones para detectar valores incorrectos tan pronto como se utilizan.Cuanto más rápido detectes un error mientras corres, menos daño podrá causar.

A continuación se muestran algunos ejemplos para ponerlo todo junto.

void showRecords(TRecordType mask) {
    assert(RecordType::IsValidMask(mask));
    // do stuff;
}

void wombleRecord(TRecord rec, TRecordType state) {
    assert(RecordType::IsValidState(state));
    if (RecordType ::xNew) {
    // ...
} in runtime

TRecordType updateRecord(TRecord rec, TRecordType newstate) {
    assert(RecordType::IsValidState(newstate));
    //...
    if (! access_was_successful) return RecordType ::xInvalid;
    return newstate;
}

La única forma de garantizar la seguridad del valor correcto es utilizar una clase dedicada con sobrecargas para el operador y eso se deja como ejercicio para otro lector.

Otros consejos

Olvídate de las definiciones

Contaminarán su código.

campos de bits?

struct RecordFlag {
    unsigned isnew:1, isdeleted:1, ismodified:1, isexisting:1;
};

Nunca uses eso.Le preocupa más la velocidad que ahorrar 4 entradas.El uso de campos de bits es en realidad más lento que el acceso a cualquier otro tipo.

Sin embargo, los miembros de bits en estructuras tienen inconvenientes prácticos.Primero, el orden de los bits en la memoria varía de un compilador a otro.Además, Muchos compiladores populares generan código ineficiente para leer y escribir miembros de bits., y hay potencialmente graves problemas de seguridad del hilo relacionados con campos de bits (especialmente en sistemas multiprocesador) debido al hecho de que la mayoría de las máquinas no pueden manipular conjuntos arbitrarios de bits en la memoria, sino que deben cargar y almacenar palabras completas.por ejemplo, lo siguiente no sería seguro para subprocesos, a pesar del uso de un mutex

Fuente: http://en.wikipedia.org/wiki/Bit_field:

Y si necesitas más razones para no usar campos de bits, tal vez Raymond Chen te convencerá en su Lo viejo y nuevo Correo: El análisis costo-beneficio de campos de bits para una colección de booleanos. en http://blogs.msdn.com/oldnewthing/archive/2008/11/26/9143050.aspx

constante constante?

namespace RecordType {
    static const uint8 xNew = 1;
    static const uint8 xDeleted = 2;
    static const uint8 xModified = 4;
    static const uint8 xExisting = 8;
}

Ponerlos en un espacio de nombres es genial.Si están declarados en su CPP o archivo de encabezado, sus valores estarán incorporados.Podrás usar el interruptor en esos valores, pero aumentará ligeramente el acoplamiento.

Ah, sí: eliminar la palabra clave estática.static está en desuso en C++ cuando se usa como lo hace usted, y si uint8 es un tipo de compilación, no necesitará esto para declararlo en un encabezado incluido en múltiples fuentes del mismo módulo.Al final el código debería ser:

namespace RecordType {
    const uint8 xNew = 1;
    const uint8 xDeleted = 2;
    const uint8 xModified = 4;
    const uint8 xExisting = 8;
}

El problema de este enfoque es que su código conoce el valor de sus constantes, lo que aumenta ligeramente el acoplamiento.

enumeración

Lo mismo que const int, con una escritura algo más fuerte.

typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;

Sin embargo, todavía están contaminando el espacio de nombres global.Por cierto... Eliminar el tipo de definición.Estás trabajando en C++.Esas definiciones de tipos de enumeraciones y estructuras están contaminando el código más que cualquier otra cosa.

El resultado es algo así como:

enum RecordType { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } ;

void doSomething(RecordType p_eMyEnum)
{
   if(p_eMyEnum == xNew)
   {
       // etc.
   }
}

Como puede ver, su enumeración está contaminando el espacio de nombres global.Si pones esta enumeración en un espacio de nombres, tendrás algo como:

namespace RecordType {
   enum Value { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } ;
}

void doSomething(RecordType::Value p_eMyEnum)
{
   if(p_eMyEnum == RecordType::xNew)
   {
       // etc.
   }
}

constante externa int?

Si desea disminuir el acoplamiento (es decir,pudiendo ocultar los valores de las constantes, y así, modificarlas como desees sin necesidad de una recompilación completa), puedes declarar los ints como externos en el encabezado, y como constantes en el archivo CPP, como en el siguiente ejemplo:

// Header.hpp
namespace RecordType {
    extern const uint8 xNew ;
    extern const uint8 xDeleted ;
    extern const uint8 xModified ;
    extern const uint8 xExisting ;
}

Y:

// Source.hpp
namespace RecordType {
    const uint8 xNew = 1;
    const uint8 xDeleted = 2;
    const uint8 xModified = 4;
    const uint8 xExisting = 8;
}

Sin embargo, no podrás activar esas constantes.Así que al final, elige tu veneno...:-pag

¿Has descartado std::bitset?Para conjuntos de banderas es para lo que sirve.Hacer

typedef std::bitset<4> RecordType;

entonces

static const RecordType xNew(1);
static const RecordType xDeleted(2);
static const RecordType xModified(4);
static const RecordType xExisting(8);

Debido a que hay un montón de sobrecargas de operadores para bitset, ahora puede hacer

RecordType rt = whatever;      // unsigned long or RecordType expression
rt |= xNew;                    // set 
rt &= ~xDeleted;               // clear 
if ((rt & xModified) != 0) ... // test

O algo muy similar a eso. Agradecería cualquier corrección ya que no lo he probado.También puede hacer referencia a los bits por índice, pero generalmente es mejor definir sólo un conjunto de constantes, y las constantes RecordType probablemente sean más útiles.

Suponiendo que haya descartado bitset, voto por el enumeración.

No creo que lanzar las enumeraciones sea una desventaja seria; está bien, entonces es un poco ruidoso y asignar un valor fuera de rango a una enumeración es un comportamiento indefinido, por lo que, en teoría, es posible dispararse en el pie con algún C++ inusual. implementaciones.Pero si sólo lo haces cuando es necesario (que es cuando pasas de int a enum iirc), es un código perfectamente normal que la gente ha visto antes.

También tengo dudas sobre el costo del espacio de la enumeración.Las variables y parámetros de uint8 probablemente no usarán menos pila que los ints, por lo que solo importa el almacenamiento en clases.Hay algunos casos en los que empaquetar varios bytes en una estructura ganará (en cuyo caso puede convertir enumeraciones dentro y fuera del almacenamiento uint8), pero normalmente el relleno anulará el beneficio de todos modos.

Por lo tanto, la enumeración no tiene desventajas en comparación con las demás y, como ventaja, le brinda un poco de seguridad de tipos (no puede asignar un valor entero aleatorio sin realizar una conversión explícita) y formas limpias de referirse a todo.

Por cierto, de preferencia también pondría "= 2" en la enumeración.No es necesario, pero un "principio de mínimo asombro" sugiere que las 4 definiciones deberían ser iguales.

Aquí hay un par de artículos sobre const vs.macros vs.enumeraciones:

Constantes simbólicas
Constantes de enumeración vs.Objetos constantes

Creo que deberías evitar las macros, especialmente porque escribiste la mayor parte de tu nuevo código en C++ moderno.

Si es posible NO utilice macros.No son muy admirados cuando se trata del C++ moderno.

Las enumeraciones serían más apropiadas ya que proporcionan "significado a los identificadores", así como seguridad de tipos.Se puede decir claramente que "xDeleted" es de "RecordType" y que representa el "tipo de registro" (¡guau!) incluso después de años.Las constantes requerirían comentarios para eso, también requerirían subir y bajar en el código.

Con define pierdo seguridad de tipo

No necesariamente...

// signed defines
#define X_NEW      0x01u
#define X_NEW      (unsigned(0x01))  // if you find this more readable...

y con enum pierdo algo de espacio (enteros)

No necesariamente, pero sí hay que ser explícito en los puntos de almacenamiento...

struct X
{
    RecordType recordType : 4;  // use exactly 4 bits...
    RecordType recordType2 : 4;  // use another 4 bits, typically in the same byte
    // of course, the overall record size may still be padded...
};

y probablemente tenga que emitir cuando quiera realizar una operación bit a bit.

Puedes crear operadores para aliviar esto:

RecordType operator|(RecordType lhs, RecordType rhs)
{
    return RecordType((unsigned)lhs | (unsigned)rhs);
}

Con const creo que también pierdo la seguridad de tipos ya que un uint8 aleatorio podría ingresar por error.

Lo mismo puede pasar con cualquiera de estos mecanismos:Las comprobaciones de rango y valor son normalmente ortogonales a la seguridad de tipos (aunque los tipos definidos por el usuario, es decir,sus propias clases - puede imponer "invariantes" sobre sus datos).Con las enumeraciones, el compilador es libre de elegir un tipo más grande para albergar los valores, y una variable de enumeración no inicializada, corrupta o simplemente mal configurada podría terminar interpretando su patrón de bits como un número que no se esperaría, comparando desigual con cualquiera de los identificadores de enumeración, cualquier combinación de ellos, y 0.

¿Existe alguna otra forma más limpia?/ Si no, ¿qué usarías y por qué?

Bueno, al final, el OR bit a bit de enumeraciones de estilo C probado y confiable funciona bastante bien una vez que tiene campos de bits y operadores personalizados en la imagen.Puede mejorar aún más su solidez con algunas funciones de validación y afirmaciones personalizadas como en la respuesta de mat_geek;Las técnicas a menudo son igualmente aplicables al manejo de cadenas, int, valores dobles, etc.

Se podría argumentar que esto es "más limpio":

enum RecordType { New, Deleted, Modified, Existing };

showRecords([](RecordType r) { return r == New || r == Deleted; });

soy indiferente:los bits de datos se empaquetan más, pero el código crece significativamente...Depende de cuántos objetos tengas, y las lamdbas, por hermosas que sean, siguen siendo más complicadas y difíciles de corregir que las OR bit a bit.

Por cierto, en mi humilde opinión, el argumento sobre la seguridad de los subprocesos es bastante débil: es mejor recordarlo como una consideración de fondo en lugar de convertirse en una fuerza dominante que impulse las decisiones;compartir un mutex entre los campos de bits es una práctica más probable incluso si no se conoce su empaquetamiento (los mutex son miembros de datos relativamente voluminosos; tengo que estar realmente preocupado por el rendimiento para considerar tener múltiples mutex en miembros de un objeto, y miraría cuidadosamente lo suficiente como para notar que eran campos de bits).Cualquier tipo de tamaño de subpalabra podría tener el mismo problema (p. ej.a uint8_t).De todos modos, podrías probar operaciones de estilo atómico de comparación e intercambio si estás desesperado por lograr una mayor concurrencia.

Incluso si tiene que usar 4 bytes para almacenar una enumeración (no estoy muy familiarizado con C++; sé que puede especificar el tipo subyacente en C#), aún así vale la pena: use enumeraciones.

En la actualidad de servidores con GB de memoria, cosas como 4 bytes vs.1 byte de memoria a nivel de aplicación en general no importa.Por supuesto, si en su situación particular, el uso de la memoria es tan importante (y no puede hacer que C++ use un byte para respaldar la enumeración), entonces puede considerar la ruta 'const estática'.

Al final del día, debe preguntarse: ¿vale la pena el mantenimiento de usar 'constancia estática' para los 3 bytes de ahorro de memoria para su estructura de datos?

Algo más a tener en cuenta: IIRC, en x86, las estructuras de datos están alineadas con 4 bytes, por lo que, a menos que tenga una cantidad de elementos de ancho de bytes en su estructura de "registro", es posible que en realidad no importe.Pruebe y asegúrese de que así sea antes de hacer un compromiso entre la mantenibilidad y el rendimiento/espacio.

Si desea la seguridad de tipos de clases, con la conveniencia de la sintaxis de enumeración y la verificación de bits, considere Etiquetas seguras en C++.He trabajado con el autor y es bastante inteligente.

Pero ten cuidado.Al final, este paquete utiliza plantillas. y macros!

¿Realmente necesita transmitir los valores de las banderas como un todo conceptual o tendrá mucho código por bandera?De cualquier manera, creo que tener esto como clase o estructura de campos de bits de 1 bit podría ser más claro:

struct RecordFlag {
    unsigned isnew:1, isdeleted:1, ismodified:1, isexisting:1;
};

Entonces su clase de registro podría tener una variable miembro de estructura RecordFlag, las funciones pueden tomar argumentos de tipo estructura RecordFlag, etc.El compilador debería empaquetar los campos de bits juntos, ahorrando espacio.

Probablemente no usaría una enumeración para este tipo de cosas en las que los valores se pueden combinar; por lo general, las enumeraciones son estados mutuamente excluyentes.

Pero sea cual sea el método que utilice, para que quede más claro que estos son valores que son bits que se pueden combinar, utilice esta sintaxis para los valores reales:

#define X_NEW      (1 << 0)
#define X_DELETED  (1 << 1)
#define X_MODIFIED (1 << 2)
#define X_EXISTING (1 << 3)

Usar un desplazamiento hacia la izquierda ayuda a indicar que cada valor está destinado a ser un solo bit, es menos probable que más adelante alguien haga algo mal, como agregar un nuevo valor y asignarle un valor de 9.

Residencia en BESO, alta cohesión y bajo acoplamiento, Haga estas preguntas -

  • ¿Quién necesita saberlo?mi clase, mi biblioteca, otras clases, otras bibliotecas, terceros
  • ¿Qué nivel de abstracción debo proporcionar?¿El consumidor comprende las operaciones de bits?
  • ¿Tendré que interactuar desde VB/C#, etc.?

Hay un gran libro "Diseño de software C++ a gran escala", esto promueve los tipos base externamente, si puede evitar otra dependencia de interfaz/archivo de encabezado, debería intentarlo.

Si estás usando Qt deberías buscar Banderas Q.La clase QFlags proporciona una forma segura de almacenar combinaciones OR de valores de enumeración.

preferiría ir con

typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;

Simplemente porque:

  1. Es más limpio y hace que el código sea legible y mantenible.
  2. Agrupa lógicamente las constantes.
  3. El tiempo del programador es más importante, a menos que su trabajo es para guardar esos 3 bytes.

No es que me guste diseñar demasiado todo, pero a veces, en estos casos, puede valer la pena crear una clase (pequeña) para encapsular esta información.Si crea una clase RecordType, entonces podría tener funciones como:

conjunto vacíoDeleted();

void clearDeleted();

bool está eliminado();

etc...(o cualquier convención que convenga)

Podría validar combinaciones (en el caso de que no todas las combinaciones sean legales, por ejemplo, si no se pueden configurar "nuevo" y "eliminado" al mismo tiempo).Si solo usó máscaras de bits, etc., entonces el código que establece el estado debe validarse, una clase también puede encapsular esa lógica.

La clase también puede brindarle la posibilidad de adjuntar información de registro significativa a cada estado, puede agregar una función para devolver una representación de cadena del estado actual, etc. (o usar los operadores de transmisión '<<').

Por todo eso, si le preocupa el almacenamiento, aún podría hacer que la clase solo tenga un miembro de datos 'char', por lo que solo tome una pequeña cantidad de almacenamiento (suponiendo que no sea virtual).Por supuesto, dependiendo del hardware, etc., es posible que tenga problemas de alineación.

Es posible que los valores de bits reales no sean visibles para el resto del "mundo" si están en un espacio de nombres anónimo dentro del archivo cpp en lugar de en el archivo de encabezado.

Si encuentra que el código que utiliza enum/#define/bitmask, etc. tiene una gran cantidad de código de "soporte" para lidiar con combinaciones no válidas, registros, etc., entonces puede valer la pena considerar la encapsulación en una clase.Por supuesto, la mayoría de las veces los problemas simples funcionan mejor con soluciones simples...

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top