Em um projeto C ++ em que estou trabalhando, tenho um tipo de sinalizador que pode ter quatro valores. Essas quatro bandeiras podem ser combinadas. Os sinalizadores descrevem os registros no banco de dados e podem ser:
- novo recorde
- registro excluído
- registro modificado
- registro existente
Agora, para cada registro, desejo manter esse atributo, para que eu possa usar uma enumeração:
enum { xNew, xDeleted, xModified, xExisting }
No entanto, em outros lugares do código, preciso selecionar quais registros serão visíveis para o usuário, portanto, gostaria de passar isso como um único parâmetro, como:
showRecords(xNew | xDeleted);
Então, parece que tenho três possíveis apelações:
#define X_NEW 0x01
#define X_DELETED 0x02
#define X_MODIFIED 0x04
#define X_EXISTING 0x08
ou
typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;
ou
namespace RecordType {
static const uint8 xNew = 1;
static const uint8 xDeleted = 2;
static const uint8 xModified = 4;
static const uint8 xExisting = 8;
}
Os requisitos de espaço são importantes (byte vs int), mas não são cruciais. Com define, perco a segurança do tipo, e com enum
perco algum espaço (inteiros) e provavelmente tenho que converter quando quero fazer uma operação bit a bit. Com const
eu acho que também perco a segurança do tipo, pois um aleatório uint8
pode entrar por engano.
Existe alguma outra maneira mais limpa?
Caso contrário, o que você usaria e por quê?
PS O restante do código é um C ++ moderno bastante limpo, sem #define
s, e eu usei namespaces e templates em poucos espaços, portanto esses também não estão fora de questão.
fonte
enum RecordType : uint8_t
combina o tipo de segurançaenum
com o tamanho pequeno deuint8_t
, embora você ainda precise fornecer aos operadores um bit a bit.Respostas:
Combine as estratégias para reduzir as desvantagens de uma única abordagem. Como trabalho em sistemas embarcados, a solução a seguir é baseada no fato de que operadores inteiros e bit a bit são rápidos, com pouca memória e pouco uso de flash.
Coloque a enumeração em um espaço para nome para impedir que as constantes poluam o espaço para nome global.
Uma enum declara e define um tempo de compilação verificado digitado. Sempre use a verificação do tipo de tempo de compilação para garantir que argumentos e variáveis recebam o tipo correto. Não há necessidade do typedef em C ++.
Crie outro membro para um estado inválido. Isso pode ser útil como código de erro; por exemplo, quando você deseja retornar o estado, mas a operação de E / S falha. Também é útil para depuração; use-o nas listas de inicialização e destruidores para saber se o valor da variável deve ser usado.
Considere que você tem dois propósitos para esse tipo. Para rastrear o estado atual de um registro e criar uma máscara para selecionar registros em determinados estados. Crie uma função embutida para testar se o valor do tipo é válido para sua finalidade; como um marcador de estado vs uma máscara de estado. Isso detectará bugs, pois o valor
typedef
é apenas umint
e um valor que0xDEADBEEF
pode estar na sua variável por meio de variáveis não inicializadas ou mal direcionadas.Adicione uma
using
diretiva se desejar usar o tipo com frequência.As funções de verificação de valor são úteis nas afirmações para interceptar valores ruins assim que são usados. Quanto mais rápido você pegar um bug durante a execução, menos danos ele poderá causar.
Aqui estão alguns exemplos para juntar tudo.
A única maneira de garantir a segurança correta dos valores é usar uma classe dedicada com sobrecargas do operador e isso é deixado como exercício para outro leitor.
fonte
IsValidMask
não permita a seleção de nenhum (ie0
)?Esqueça as definições
Eles poluirão seu código.
campos de bits?
Nunca use isso . Você está mais preocupado com a velocidade do que com a economia de 4 polegadas. Usar campos de bits é realmente mais lento que o acesso a qualquer outro tipo.
Fonte: http://en.wikipedia.org/wiki/Bit_field :
E se você precisar de mais motivos para não usar campos de bits, talvez Raymond Chen o convença em The Old New Thing Post: A análise de custo-benefício de campos de bits para uma coleção de booleanos em http://blogs.msdn.com/oldnewthing/ archive / 2008/11/26 / 9143050.aspx
const int?
Colocá-los em um espaço para nome é legal. Se eles forem declarados no seu CPP ou arquivo de cabeçalho, seus valores serão incorporados. Você poderá usar esses valores, mas isso aumentará um pouco o acoplamento.
Ah, sim: remova a palavra-chave estática . static está obsoleto no C ++ quando usado como você e, se uint8 for do tipo buildin, não será necessário declarar isso em um cabeçalho incluído por várias fontes do mesmo módulo. No final, o código deve ser:
O problema dessa abordagem é que seu código conhece o valor de suas constantes, o que aumenta um pouco o acoplamento.
enum
O mesmo que const int, com uma digitação um pouco mais forte.
Eles ainda estão poluindo o espaço para nome global, no entanto. A propósito ... Remova o typedef . Você está trabalhando em C ++. Esses typedefs de enumerações e estruturas estão poluindo o código mais do que qualquer outra coisa.
O resultado é meio que:
Como você vê, sua enumeração está poluindo o espaço para nome global. Se você colocar esse enum em um espaço para nome, terá algo como:
extern const int?
Se você quiser diminuir o acoplamento (ou seja, poder ocultar os valores das constantes e modificá-los conforme desejado, sem precisar de uma recompilação completa), poderá declarar as entradas como externas no cabeçalho e constantes no arquivo CPP , como no exemplo a seguir:
E:
Você não poderá usar o switch nessas constantes. Então, no final, escolha seu veneno ... :-p
fonte
Você descartou std :: bitset? Conjuntos de bandeiras é o que serve. Faz
então
Como existem várias sobrecargas de operador para o conjunto de bits, agora você pode fazer
Ou algo muito semelhante a isso - eu apreciaria qualquer correção, pois não testei isso. Você também pode consultar os bits por índice, mas geralmente é melhor definir apenas um conjunto de constantes, e as constantes RecordType são provavelmente mais úteis.
Supondo que você tenha descartado o bitset, voto a favor da enumeração .
Eu não acredito que lançar enums seja uma séria desvantagem - OK, então é um pouco barulhento e atribuir um valor fora do intervalo a um enum é um comportamento indefinido, portanto é teoricamente possível dar um tiro no pé em algum C ++ incomum implementações. Mas se você fizer isso apenas quando necessário (que é quando passa de int para enum iirc), é um código perfeitamente normal que as pessoas já viram antes.
Também duvido de qualquer custo de espaço do enum. As variáveis e parâmetros do uint8 provavelmente não usarão menos pilha do que ints, portanto, apenas o armazenamento nas classes é importante. Existem alguns casos em que o empacotamento de vários bytes em uma estrutura vencerá (nesse caso, você poderá converter enums dentro e fora do armazenamento uint8), mas normalmente o preenchimento eliminará o benefício de qualquer maneira.
Portanto, o enum não tem desvantagens em comparação com os outros e, como vantagem, oferece um pouco de segurança de tipo (você não pode atribuir um valor inteiro aleatório sem converter explicitamente) e maneiras limpas de se referir a tudo.
De preferência, eu também colocaria o "= 2" na enumeração, a propósito. Não é necessário, mas um "princípio de menor espanto" sugere que todas as quatro definições devem ter a mesma aparência.
fonte
bitset
para isso? ele geralmente se traduz em umlong
(na minha implementação iirc; sim, que desperdício) ou tipo integral semelhante para cada elemento, então porque não usar apenas integrais não ofuscadas? (ou hoje em dia,constexpr
com armazenamento zero)bitset
classe, além do que parece ser uma tendência recorrente nas discussões em torno de 'ugh, precisamos encobrir as raízes desagradáveis de baixo nível da linguagem ''uint8
variáveis e parâmetros provavelmente não usarão menos pilha do queints
" está errado. Se você possui uma CPU com registradores de 8 bits, sãoint
necessários pelo menos 2 registradores euint8_t
apenas 1, portanto, será necessário mais espaço de pilha, pois é mais provável que você esteja sem registradores (o que também é mais lento e pode aumentar o tamanho do código ( dependendo do conjunto de instruções)). (Você tem um tipo, deve seruint8_t
nãouint8
)Aqui estão alguns artigos sobre const vs. macros vs. enums:
Constantes simbólicas Constantes de
enumeração vs. objetos constantes
Eu acho que você deve evitar macros, especialmente porque você escreveu a maior parte do seu novo código em C ++ moderno.
fonte
Se possível, NÃO use macros. Eles não são muito admirados quando se trata de C ++ moderno.
fonte
As enums seriam mais apropriadas, pois fornecem "significado aos identificadores" e também segurança de tipo. Você pode dizer claramente que "xDeleted" é de "RecordType" e que representa "tipo de registro" (uau!), Mesmo depois de anos. Os consts exigiriam comentários para isso, também exigiriam subir e descer no código.
fonte
Não necessariamente...
Não necessariamente - mas você precisa ser explícito nos pontos de armazenamento ...
Você pode criar operadores para aliviar isso:
O mesmo pode acontecer com qualquer um desses mecanismos: as verificações de alcance e valor são normalmente ortogonais para digitar segurança (embora os tipos definidos pelo usuário - ou seja, suas próprias classes - possam impor "invariantes" a seus dados). Com enumerações, o compilador é livre para escolher um tipo maior para hospedar os valores, e uma variável de enumer não inicializada, corrompida ou com apenas um conjunto incorreto ainda pode acabar interpretando seu padrão de bits como um número que você não esperaria - comparando desigual a qualquer um dos os identificadores de enumeração, qualquer combinação deles e 0.
Bem, no final, o OR de bit a bit, experimentado e confiável, no estilo C, funciona muito bem quando você tem campos de bits e operadores personalizados na imagem. Você pode melhorar ainda mais sua robustez com algumas funções e asserções de validação personalizadas, como na resposta de mat_geek; técnicas geralmente igualmente aplicáveis ao manuseio de strings, int, valores duplos etc.
Você poderia argumentar que isso é "mais limpo":
Sou indiferente: os bits de dados são mais compactos, mas o código cresce significativamente ... depende de quantos objetos você tem, e os lamdbas - por mais bonitos que sejam - ainda são mais confusos e difíceis de acertar do que as ORs bit a bit.
BTW / - o argumento sobre o IMHO bastante fraco da segurança de threads - deve ser lembrado como uma consideração de segundo plano, em vez de se tornar uma força dominante na tomada de decisões; compartilhar um mutex entre os campos de bits é uma prática mais provável, mesmo que não tenha conhecimento de sua compactação (os mutexes são membros de dados relativamente volumosos - eu tenho que estar realmente preocupado com o desempenho para considerar a possibilidade de ter vários mutexes nos membros de um objeto, e eu observaria cuidadosamente o suficiente para notar que eram campos de bits). Qualquer tipo de tamanho de sub-palavra pode ter o mesmo problema (por exemplo, a
uint8_t
). De qualquer forma, você pode tentar operações atômicas no estilo de comparar e trocar se estiver desesperado por uma simultaneidade mais alta.fonte
operator|
deve converter para um tipo inteiro (unsigned int
) antes da instrução|
. Casooperator|
contrário, a chamada se chamará recursivamente e causará um estouro de pilha em tempo de execução. Eu sugiro:return RecordType( unsigned(lhs) | unsigned(rhs) );
. CheersMesmo se você precisar usar 4 bytes para armazenar uma enumeração (não conheço o C ++ - sei que é possível especificar o tipo subjacente em C #), ainda vale a pena - use enumerações.
Hoje em dia, os servidores com GBs de memória, coisas como 4 bytes vs. 1 byte de memória no nível do aplicativo em geral não importam. Obviamente, se em sua situação específica, o uso de memória é tão importante (e você não pode fazer com que o C ++ use um byte para fazer backup da enumeração), considere a rota 'estática const'.
No final do dia, você deve se perguntar: vale a pena o trabalho de manutenção usando 'const estática' para os 3 bytes de economia de memória para sua estrutura de dados?
Outra coisa a ter em mente - o IIRC, no x86, as estruturas de dados são alinhadas em 4 bytes, portanto, a menos que você tenha vários elementos de largura de bytes em sua estrutura de 'registro', isso pode não ser realmente importante. Teste e certifique-se de que sim antes de fazer uma troca na capacidade de manutenção por desempenho / espaço.
fonte
int
menos que seja muito pequeno". [Se você não especificar o tipo subjacente no C ++ 11, ele usará o comportamento herdado. Por outro lado, oenum class
tipo subjacente do C ++ 11 padroniza explicitamenteint
se não for especificado de outra forma.]Se você deseja o tipo de segurança das classes, com a conveniência da sintaxe da enumeração e da verificação de bits, considere Rótulos Seguros em C ++ . Eu trabalhei com o autor, e ele é muito inteligente.
Cuidado, no entanto. No final, este pacote usa modelos e macros!
fonte
Você realmente precisa passar os valores da flag como um todo conceitual ou terá muito código por flag? De qualquer forma, acho que ter isso como classe ou estrutura de campos de bits de 1 bit pode ser mais claro:
Em seguida, sua classe de registro pode ter uma variável de membro struct RecordFlag, funções podem receber argumentos do tipo struct RecordFlag, etc. O compilador deve agrupar os campos de bits, economizando espaço.
fonte
Eu provavelmente não usaria uma enumeração para esse tipo de coisa em que os valores podem ser combinados, mais tipicamente enumerações são estados mutuamente exclusivos.
Porém, seja qual for o método usado, para deixar mais claro que esses são valores que são bits que podem ser combinados, use esta sintaxe para os valores reais:
Usar um deslocamento para a esquerda ajuda a indicar que cada valor se destina a ser um único bit, é menos provável que mais tarde alguém faça algo errado, como adicionar um novo valor e atribuir a ele um valor 9.
fonte
Com base no KISS , alta coesão e baixo acoplamento , faça estas perguntas -
Existe um ótimo livro " Design de software C ++ em larga escala ", que promove tipos de base externamente, se você puder evitar outra dependência de interface / arquivo de cabeçalho que deve tentar.
fonte
Se você estiver usando o Qt, procure QFlags . A classe QFlags fornece uma maneira segura de armazenar as combinações OR de valores de enumeração.
fonte
Eu prefiro ir com
Simplesmente porque:
fonte
Não que eu goste de projetar demais tudo, mas, às vezes, nesses casos, pode valer a pena criar uma classe (pequena) para encapsular essas informações. Se você criar uma classe RecordType, ela poderá ter funções como:
void setDeleted ();
void clearDeleted ();
bool isDeleted ();
etc ... (ou qualquer que seja a convenção)
Poderia validar combinações (no caso em que nem todas as combinações são legais, por exemplo, se 'novo' e 'excluído' não pudessem ser definidos ao mesmo tempo). Se você acabou de usar máscaras de bits etc., o código que define o estado precisa ser validado, uma classe também pode encapsular essa lógica.
A classe também pode dar a você a capacidade de anexar informações de log significativas a cada estado; você pode adicionar uma função para retornar uma representação em cadeia do estado atual, etc. (ou usar os operadores << »).
Por tudo isso, se você está preocupado com o armazenamento, você ainda pode ter a classe apenas com um membro de dados 'char', portanto, leve apenas uma pequena quantidade de armazenamento (supondo que não seja virtual). Obviamente, dependendo do hardware, etc., você pode ter problemas de alinhamento.
Você pode ter os valores de bits reais não visíveis para o resto do 'mundo' se eles estiverem em um espaço para nome anônimo dentro do arquivo cpp, e não no arquivo de cabeçalho.
Se você achar que o código que usa o enum / # define / bitmask etc tem muito código de 'suporte' para lidar com combinações inválidas, registros etc., então o encapsulamento em uma classe pode ser considerado. É claro que na maioria das vezes problemas simples são melhores com soluções simples ...
fonte