¿Debo usar #define, enum o const?

En un proyecto de C ++ en el que estoy trabajando, tengo un tipo de valor de indicador que puede tener cuatro valores. Esos cuatro indicadores se pueden combinar. Los indicadores 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í podría usar una enumeración:

enum { xNew, xDeleted, xModified, xExisting } 

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

 showRecords(xNew | xDeleted); 

Entonces, parece que tengo tres posibles candidatos:

 #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 frente a int) pero no son cruciales. Con define I lose type safety, y con enum pierdo algo de espacio (enteros) y probablemente tenga que lanzar cuando quiero hacer una operación bit a bit. const creo que también pierdo seguridad de tipo ya que un uint8 azar podría entrar por error.

¿Hay alguna otra manera más limpia?

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

PD: El rest del código es bastante moderno y limpio, sin #define s, y he usado espacios de nombres y plantillas en pocos espacios, por lo que tampoco están fuera de discusión.

Combine las estrategias para reducir las desventajas de un enfoque único. Trabajo en sistemas integrados, por lo que la siguiente solución se basa en el hecho de que los operadores enteros y en modo bit son rápidos, con poca memoria y bajo consumo de flash.

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

 namespace RecordType { 

Un enum declara y define un tiempo de comstackción marcado typescript. Utilice siempre la verificación de tipo de tiempo de comstackción para asegurarse de que los argumentos y las variables tengan el tipo correcto. No hay necesidad de typedef en C ++.

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

Crear otro miembro para un estado no válido. Esto puede ser ú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 la depuración; Úselo en listas de inicialización y destructores para saber si se debe usar el valor de la variable.

 xInvalid = 16 }; 

Considere que tiene 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 un marcador de estado frente a una máscara de estado. Esto atrapará errores ya que typedef es solo un int y un valor como 0xDEADBEEF puede estar en su variable a través de variables no inicializadas o mal definidas.

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

Agregue una directiva de using si desea utilizar el tipo con frecuencia.

 using RecordType ::TRecordType ; 

Las funciones de comprobación del valor son útiles en afirmaciones para atrapar valores incorrectos tan pronto como se utilizan. Cuanto más rápido detecte un error al correr, menos daño puede hacer.

Aquí hay algunos ejemplos para poner 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 manera de garantizar la seguridad de los valores correctos es usar una clase dedicada con sobrecargas del operador y eso se deja como ejercicio para otro lector.

Olvida los define

Ellos contaminarán tu código.

bitfields?

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

Nunca uses eso . Le preocupa más la velocidad que la de economizar 4 ints. Usar campos de bit es en realidad más lento que el acceso a cualquier otro tipo.

Sin embargo, los miembros de bit en las estructuras tienen inconvenientes prácticos. En primer lugar, el orden de los bits en la memoria varía desde el comstackdor hasta el comstackdor. Además, muchos comstackdores populares generan código ineficiente para leer y escribir miembros de bit , y hay problemas de seguridad de subproceso potencialmente graves relacionados con campos de bit (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. pero en su lugar debe 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 necesita más razones para no usar bitfields, quizás Raymond Chen lo convenza en su The Old New Thing Post: El análisis de costo-beneficio de bitfields para una colección de booleanos en http://blogs.msdn.com/oldnewthing/ archive / 2008/11/26 / 9143050.aspx

const int?

 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 se declaran en su CPP o archivo de encabezado, sus valores estarán en línea. Podrá utilizar estos valores, pero boostá ligeramente el acoplamiento.

Ah, sí: elimine la palabra clave estática . static está en desuso en C ++ cuando se usa como lo hace, y si uint8 es un tipo de comstackción, no será necesario declarar esto en un encabezado incluido por varias fonts 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.

enum

Lo mismo que const int, con un tipado 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 … elimina el typedef . Estás trabajando en C ++. Esos tipos de enumeraciones de enums y structs están contaminando el código más que cualquier otra cosa.

El resultado es un poco:

 enum RecordType { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } ; void doSomething(RecordType p_eMyEnum) { if(p_eMyEnum == xNew) { // etc. } } 

Como ve, 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. } } 

extern const int?

Si desea disminuir el acoplamiento (es decir, puede ocultar los valores de las constantes y modificarlas como desee sin necesidad de una recomstackción completa), puede declarar las entradas como externas 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 usar el interruptor de esas constantes. Entonces, al final, elige tu veneno … :-p

¿Has descartado std :: bitset? Juegos de banderas es para lo que es. 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 he probado esto. También puede referirse a los bits por índice, pero generalmente es mejor definir solo un conjunto de constantes, y las constantes RecordType son probablemente más útiles.

Suponiendo que hayas descartado bitset, yo voto por la enumeración .

No creo que el casting de las enumeraciones sea una desventaja seria – OK, así que es un poco ruidoso, y asignar un valor fuera de rango a una enumeración es un comportamiento indefinido, así que teóricamente es posible dispararse en el pie con un C ++ inusual implementaciones. Pero si solo lo hace cuando es necesario (que es cuando va de int a enum iirc), es un código perfectamente normal que la gente ha visto antes.

También tengo dudas sobre cualquier costo de espacio de la enumeración. Es probable que las variables y los parámetros Uint8 no usen menos stack que los ints, por lo que solo importa el almacenamiento en clases. Hay algunos casos en los que se ganarán varios bytes en una estructura (en cuyo caso puede lanzar enumeraciones dentro y fuera del almacenamiento de 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, ofrece un poco de seguridad de tipo (no se puede asignar un valor entero aleatorio sin conversión explícita) y formas claras de referirse a todo.

Para su preferencia, también pondría el “= 2” en la enumeración, por cierto. No es necesario, pero un “principio de menor asombro” sugiere que las 4 definiciones deberían tener el mismo aspecto.

Aquí hay algunos artículos sobre const vs. macros vs. enums:

Constantes simbólicas
Constantes de enumeración frente a objetos constantes

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

Si es posible, NO use macros. No son muy admirados cuando se trata de C ++ moderno.

Los enum serían más apropiados ya que proporcionan “significado a los identificadores”, así como tipo de seguridad. Puede decir claramente que “xDeleted” es de “RecordType” y que representa “tipo de registro” (¡guau!) Incluso después de años. Las ventajas requerirían comentarios para eso, también requerirían subir y bajar en el código.

Con define pierdo seguridad 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 tienes 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 transmitir cuando quiero hacer una operación bit a bit.

Puede crear operadores para aliviar el dolor de eso:

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

Const creo que también pierdo seguridad de tipo ya que un error al azar podría entrar por error.

Lo mismo puede suceder 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, pueden hacer valer “invariantes” sobre sus datos). Con enumeraciones, el comstackdor es libre de elegir un tipo más grande para alojar los valores, y una variable enum sin inicializar, corrompida o simplemente errónea podría terminar interpretando su patrón de bits como un número que no esperaría, comparando desigual a cualquiera de los identificadores de enumeración, cualquier combinación de ellos, y 0.

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

Bueno, al final, el OR a prueba de bits de tipo probada y confiable de enumeraciones funciona bastante bien una vez que tiene campos de bits y operadores personalizados en la imagen. Puede mejorar aún más su robustez con algunas funciones y aserciones de validación personalizadas como en la respuesta de mat_geek; técnicas a menudo igualmente aplicables para manejar cadenas, int, valores dobles, etc.

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 son más apretados, pero el código crece significativamente … depende de cuántos objetos hay, y los lamdbas, por bellos que sean, aún son más complicados y difíciles de obtener que los bits bit a bit.

Por cierto, el argumento sobre mi humilde seguridad en mi humilde opinión es mejor recordado como una consideración de fondo en lugar de convertirse en una fuerza motriz dominante; compartir una mutex en todos los campos de bit es una práctica más probable incluso si desconocen su empaquetamiento (los mutex son miembros de datos relativamente voluminosos; tengo que estar realmente preocupado por el rendimiento para considerar tener mutexes múltiples en los miembros de un objeto, y mirar con atención suficiente para notar que eran campos de bit). Cualquier tipo de tamaño de palabra inferior podría tener el mismo problema (por ejemplo, un uint8_t ). De todos modos, podrías probar operaciones atómicas de comparación y cambio si estás desesperado por una mayor concurrencia.

Incluso si tiene que usar 4 bytes para almacenar una enumeración (no estoy tan familiarizado con C ++, sé que puede especificar el tipo subyacente en C #), todavía lo vale, use enumeraciones.

En esta época de servidores con GB de memoria, cosas como 4 bytes frente a 1 byte de memoria en el nivel de aplicación en general no importan. Por supuesto, si en su situación particular, el uso de 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, tiene que preguntarse: ¿vale la pena el mantenimiento de usar ‘static const’ 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 en 4 bytes, por lo que a menos que tenga un número de elementos de ancho de bytes en su estructura ‘registro’, puede que no importe. Pruebe y asegúrese de hacerlo antes de realizar un intercambio en la mantenibilidad del rendimiento / espacio.

Si desea el tipo de seguridad de las clases, con la conveniencia de la syntax de enumeración y la comprobación de bits, considere Safe Labels in C ++ . He trabajado con el autor, y él es bastante inteligente.

Ten cuidado, sin embargo. ¡Al final, este paquete usa plantillas y macros!

¿De verdad necesita pasar los valores de la bandera como un todo conceptual, o va a tener un montón de código por bandera? De cualquier manera, creo que tener esto como clase o estructura de bitfields 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 de estructura struct RecordFlag, las funciones pueden tomar argumentos de tipo struct RecordFlag, etc. El comstackdor debe empaquetar los bitfield juntos, ahorrando espacio.

Probablemente no use una enumeración para este tipo de cosas donde los valores se pueden combinar, más comúnmente las enumeraciones son estados mutuamente excluyentes.

Pero cualquiera que sea el método que use, para dejar en claro que estos son valores que son bits que se pueden combinar, use en su lugar esta syntax 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 incorrecto como agregar un nuevo valor y asignarle un valor de 9.

Basado en KISS , alta cohesión y bajo acoplamiento , haga estas preguntas:

  • ¿Quién necesita saber? mi clase, mi biblioteca, otras clases, otras bibliotecas, terceros
  • ¿Qué nivel de abstracción necesito proporcionar? ¿El consumidor entiende las operaciones de bits?
  • ¿Tendré que hacer una interfaz desde VB / C #, etc.?

Existe un gran libro ” Diseño de software de C ++ a gran escala “, que promueve los tipos de base externamente, si puede evitar otra dependencia de archivo de cabecera / interfaz que debe intentar.

Si está utilizando Qt, debería buscar QFlags . La clase QFlags proporciona una forma segura de tipo de almacenar combinaciones OR de valores enum.

Prefiero 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 progtwigdor es más importante, a menos que su trabajo sea guardar esos 3 bytes.

No es que me guste sobre-diseñar 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 puede tener funciones como:

void setDeleted ();

void clearDeleted ();

bool isDeleted ();

etc … (o cualquier traje de convención)

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

La clase también puede darle la capacidad 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 está preocupado por el almacenamiento, todavía podría hacer que la clase solo tenga un miembro de datos ‘char’, por lo que solo debe tomar una pequeña cantidad de almacenamiento (suponiendo que no sea virtual). Por supuesto, dependiendo del hardware, etc., puede haber problemas de alineación.

Podría tener los valores de bit reales no visibles para el rest del ‘mundo’ si están en un espacio de nombre anónimo dentro del archivo cpp en lugar de en el archivo de encabezado.

Si encuentra que el código que utiliza la enumeración / # define / bitmask, etc. tiene una gran cantidad de código de “soporte” para tratar con combinaciones no válidas, el registro, etc., puede valer la pena considerar la encapsulación en una clase. Por supuesto, la mayoría de las veces los problemas simples son mejores con soluciones simples …