Pergunta

Em um C ++ projeto que estou trabalhando, eu tenho um bandeira tipo de valor que pode ter quatro valores. Estas quatro sinalizadores podem ser combinados. Bandeiras descrever os registros no banco de dados e pode ser:

  • novo recorde
  • registro excluído
  • registro modificado
  • registro existente

Agora, para cada registro que deseja manter este atributo, para que eu pudesse usar uma enumeração:

enum { xNew, xDeleted, xModified, xExisting }

No entanto, em outros lugares no código, eu preciso selecionar quais registros devem ser visíveis para o usuário, então eu gostaria de ser capaz de passar isso como um único parâmetro, como:

showRecords(xNew | xDeleted);

Assim, parece que eu tenho três appoaches possíveis:

#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 crucial. Com define perco segurança de tipo e com enum eu perder algum espaço (inteiros) e, provavelmente, tem que elenco quando eu quiser fazer uma operação bit a bit. Com const eu acho que também o tipo de perder a segurança uma vez que um uint8 aleatório poderia entrar por engano.

Existe alguma outra maneira mais limpa?

Se não, o que você usa e por quê?

P.S. O resto do código é C moderna em vez impecáveis ??++ sem #defines, e eu usei namespaces e modelos em alguns espaços, por isso aqueles que não estão fora de questão qualquer um.

Foi útil?

Solução

Combine as estratégias para reduzir as desvantagens de uma abordagem única. Eu trabalho em sistemas embarcados assim que a seguinte solução baseia-se no facto dos operadores inteiros e bit a bit são rápidos, memória baixa e baixa no uso de flash.

Coloque o enum em um namespace para evitar as constantes de poluir o namespace global.

namespace RecordType {

Um enum declara e define um tempo de compilação verificado digitado. Sempre use compilação verificação de tipo tempo para certificar-se de argumentos e variáveis ??são dados do tipo correto. Não há nenhuma necessidade para o typedef em C ++.

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

Criar outro membro para um estado inválido. Isso pode ser útil como o código de erro; por exemplo, quando você quiser retornar o estado, mas a operação I / O falhar. É também útil para a depuração; usá-lo em listas de inicialização e destruidores de saber se o valor da variável deve ser usado.

xInvalid = 16 };

Considere que você tem duas finalidades deste tipo. Para acompanhar o estado atual de um registro e para criar uma máscara para selecionar registros em determinados estados. Criar uma função inline para testar se o valor do tipo é válido para a sua finalidade; como um marcador estado vs uma máscara de estado. Isso vai pegar os bugs como o typedef é apenas um int e um valor como 0xDEADBEEF pode estar em sua variável através de variáveis ??uninitialised ou mispointed.

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

Adicione uma diretiva using se você quiser usar o tipo de frequência.

using RecordType ::TRecordType ;

O valor verificando funções estão a ser útil afirma a valores maus armadilha assim que eles são usados. Quanto mais rápido você pegar um bug quando executado, menos danos ele pode fazer.

Aqui estão alguns exemplos para colocá-lo todos juntos.

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

A única maneira de garantir a segurança valor correto é usar uma classe dedicado com sobrecargas de operador e que é deixada como um exercício para outro leitor.

Outras dicas

Esqueça o define

Eles vão poluir o seu código.

bitfields?

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

Nunca usar esse . Você está mais preocupado com a velocidade do que com economizar 4 ints. Usando campos de bits é realmente mais lento do que o acesso a qualquer outro tipo.

No entanto, os membros bit em estruturas têm inconvenientes práticos. Primeiro, a ordenação de bits na memória varia de compilador para compilador. Além disso, muitos compiladores populares gerar código ineficiente para ler e escrever membros bit , e há potencialmente graves questões de segurança fio relativas a campos de bits (especialmente em sistemas com múltiplos processadores) devido à o fato de que a maioria das máquinas não pode manipular conjuntos de bits arbitrários na memória, mas deve carregar e armazenar palavras inteiras. por exemplo o seguinte não seria thread-safe, apesar do uso de um mutex

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

E se você precisar de mais razões para não uso bitfields, talvez Raymond Chen irá convencê-lo em sua The Old New Thing Post: A análise de custo-benefício de bitfields para uma coleção de booleans em 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;
}

Colocá-los em um namespace é legal. Se eles são declarados na sua CPP ou arquivo de cabeçalho, seus valores será embutido. Você vai ser capaz de usar ligar esses valores, mas vai aumentar ligeiramente o acoplamento.

Ah, sim: remover o estático palavra-chave . estática é depreciado em C ++ quando usado como você faz, e se uint8 é um tipo buildin, você não precisa disso para declarar isso em um cabeçalho incluído por várias fontes do mesmo módulo. No final, o código deve ser:

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

O problema dessa abordagem é que seu código sabe o valor de suas constantes, o que aumenta ligeiramente o acoplamento.

enum

O mesmo que int const, com uma digitação um pouco mais forte.

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

Eles ainda estão poluindo o namespace global, no entanto. A propósito ... Remover o typedef . Você está trabalhando em C ++. Esses typedefs de enums e estruturas estão poluindo o código mais do que qualquer outra coisa.

O resultado é meio:

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

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

Como você vê, a sua enumeração está poluindo o namespace global. Se você colocar este enum em um namespace, você terá 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?

Se você quer diminuir o acoplamento (isto é, ser capaz de esconder os valores das constantes, e assim, modificá-los como desejado sem a necessidade de uma recompilação completa), você pode declarar os ints como externo no cabeçalho, e tão constante em o arquivo CPP, como no exemplo a seguir:

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

E:

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

Você não será capaz de usar ligar essas constantes, no entanto. Então, no final, escolha o seu veneno ... :-P

Você descartou std :: bitset? Conjuntos de bandeiras é o que é para. Faça

typedef std::bitset<4> RecordType;

então

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

Porque há um monte de sobrecargas de operador para bitset, você pode agora fazer

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

Ou algo muito semelhante ao que - Eu apreciaria quaisquer correções desde que eu não testei isso. Você também pode se referir aos bits de índice, mas é geralmente melhor para definir apenas um conjunto de constantes e constantes RecordType são provavelmente mais útil.

Assumindo que você descartou bitset, eu voto para o enum .

Eu não comprar aquele lançando os enums é uma séria desvantagem - OK por isso é um pouco barulhento, e atribuindo um valor de out-of-range para um ENUM é um comportamento indefinido por isso é teoricamente possível para atirar no próprio pé em implementações alguns incomum C ++. Mas se você fazê-lo apenas quando necessário (que é quando vai de int para enum IIRC), é código perfeitamente normal que as pessoas tenham visto antes.

Estou em dúvida sobre qualquer custo do espaço do enum, também. variáveis ??uint8 e parâmetros provavelmente não vai usar menos pilha de ints, portanto, apenas de armazenamento em matéria classes. Existem alguns casos em que a embalagem vários bytes em um struct vai ganhar (caso em que você pode lançar enums dentro e fora de armazenamento uint8), mas normalmente preenchimento vai matar o benefício de qualquer maneira.

Assim, a enumeração não tem desvantagens em comparação com os outros, e como uma vantagem dá-lhe um pouco de tipo de segurança (não é possível atribuir algum valor inteiro aleatório sem lançar explicitamente) e formas limpas de se referir a tudo.

De preferência Eu também colocar o "= 2" na enumeração, pelo caminho. Não é necessário, mas um "princípio da menor surpresa" sugere que todos os 4 definições devem ter a mesma aparência.

Aqui estão alguns artigos sobre const vs. macros vs. enums:

constantes simbólicas
Enumeration Constantes vs. Constante objetos

Eu acho que você deve evitar macros especialmente desde que você escreveu a maior parte de seu novo código está em C ++ moderno.

Se possível, não utilizar macros. Eles não são muito admirado quando se trata de C ++ moderno.

Enums seria mais apropriado uma vez que fornecem "significado para os identificadores", bem como a segurança de tipos. Você pode dizer claramente "xDeleted" é de "RecordType" e que representam "tipo de um registro" (uau!) Mesmo depois de anos. Consts exigiria comentários para que, também eles exigiria indo para cima e para baixo no código.

Com define que tipo de perder a segurança

Não necessariamente ...

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

e com enum eu perder algum espaço (inteiros)

Não necessariamente - mas você tem que ser explícito nos pontos de armazenamento ...

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

e, provavelmente, tem que elenco quando eu quero fazer operação bit a bit.

Você pode criar operadores para tirar a dor de que:

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

Com const Eu acho que também perdem a segurança de tipos, desde um uint8 aleatório poderia entrar por engano.

O mesmo pode acontecer com qualquer um destes mecanismos: cheques alcance e valor são normalmente ortogonal ao tipo de segurança (embora definidas pelo utilizador-tipos - isto é, suas próprias classes - pode impor "invariantes" sobre seus dados). Com enums, do livre compilador para escolher um tipo maior para sediar os valores, e um uninitialised, corrompido ou apenas variável enum miss-set ainda pode acabar interpretting seu padrão de bits como um número não seria de esperar - comparando desigual a qualquer um os identificadores de enumeração, qualquer combinação deles, e 0.

Existe alguma outra maneira mais limpa? / Se não, o que você usa e por quê?

Bem, no final o bit a bit de estilo C tentou-e-confiável ou de enumerações funciona muito bem quando você tem campos de bits e operadores personalizados na imagem. Você pode melhorar ainda mais a sua robustez com algumas funções de validação personalizada e afirmações como na resposta de mat_geek; técnicas muitas vezes igualmente aplicáveis ??no manuseamento de string, int, valores duplas etc ..

Você pode argumentar que este é mais "limpo":

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

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

Eu sou indiferente: os bits de dados embalar mais apertado, mas o código cresce significativamente ... depende quantos objetos você tem, e os lamdbas - bonito como eles são - são ainda mais confusa e mais difícil de obter direito de RUP bit a bit .

BTW / - o argumento sobre a segurança do thread é muito fraco IMHO - melhor lembrado como uma consideração fundo, em vez de se tornar uma força-motriz decisão dominante; a partilha de um mutex através das bitfields é uma prática mais provável mesmo que inconscientes de sua embalagem (mutexes são membros de dados relativamente volumosos - Eu tenho que estar realmente preocupado com o desempenho de considerar ter vários semáforos em membros de um objeto, e eu olhar com cuidado o suficiente para notar que eles eram campos de bits). Qualquer tipo de sub-palavra-tamanho poderia ter o mesmo problema (por exemplo, um uint8_t). De qualquer forma, você poderia tentar operações de estilo comparar-and-swap atômicas se você está desesperado para uma maior concorrência.

Mesmo se você tem que usar 4 bytes para armazenar um enum (Eu não sou tão familiarizado com C ++ - Eu sei que você pode especificar o tipo subjacente em C #), é ainda vale a pena -. Uso enums

Neste dia e idade de 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 importa. Claro que, se em sua situação particular, o uso de memória é tão importante (e você não pode começar C ++ para usar um byte para apoiar a enumeração), então você pode considerar a rota 'static const'.

No final do dia, você tem que perguntar a si mesmo, vale a pena o hit manutenção do uso de 'const estática' para os 3 bytes de economia de memória para a sua estrutura de dados?

Outra coisa a ter em mente - IIRC, em x86, estruturas de dados são 4 bytes alinhados, então a menos que você tem um número de elementos de bytes de largura em sua estrutura 'record', talvez não realmente importa. Teste e certifique-se que ele faz antes de fazer uma troca na manutenção de performance / espaço.

Se você quiser a segurança de tipos de aulas, com a conveniência de sintaxe enumeração e verificação bit, considere Labels seguro em C ++ . Eu trabalhei com o autor, e ele é muito inteligente.

Cuidado, porém. No final, isso usa pacotes de modelos e macros!

Você realmente precisa para passar em torno dos valores bandeira como um todo conceitual, ou você vai ter um monte de código per-bandeira? De qualquer forma, eu acho que ter isso como classe ou struct de bitfields de 1 bit pode realmente ser mais claro:

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

Em seguida, a sua classe de registro poderia ter uma estrutura variável de membro RecordFlag, as funções podem levar argumentos do tipo struct RecordFlag, etc. O compilador deve embalar os bitfields juntos, economizando espaço.

Eu provavelmente não iria usar uma enumeração para este tipo de coisa em que os valores podem ser combinados em conjunto, mais tipicamente enums são estados mutuamente exclusivos.

Mas qualquer que seja o método utilizado, para torná-lo mais claro que estes são valores que são os bits que podem ser combinados em conjunto, utilize esta sintaxe para os valores reais em vez disso:

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

Usando um desvio para a esquerda não ajuda a indicar que cada valor é destinado a ser um único bit, é menos provável que, mais tarde, alguém faria algo errado como adicionar um novo valor e atribuí-lo algo um valor de 9.

Com base BEIJO , alta coesão e baixo acoplamento , faça estas perguntas -

  • Quem precisa saber? minha classe, minha biblioteca, outras classes, outras bibliotecas, 3 partes
  • O nível de abstração que eu preciso para fornecer? Será que o consumidor entender operações de bit.
  • Será que eu tenho para fazer a interface do VB / C # etc?

Há um grande livro " Large-Scale C ++ Design de Software " , este promove tipos base externamente, se você pode evitar outro cabeçalho do arquivo / interface de dependência que você deve tentar.

Se você estiver usando Qt você deve ter um olhar para QFlags . A classe QFlags fornece uma maneira segura de tipo de armazenamento ou combinações de valores enum.

Eu preferia ir com

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

Simplesmente porque:

  1. É mais limpo e faz a leitura do código e sustentável.
  2. agrupa logicamente os constantes.
  3. tempo do programador é mais importante, a menos que seu trabalho é para salvar esses 3 bytes.

Não que eu gostaria de sobre-engenheiro de tudo, mas, por vezes, nestes casos, pode valer a pena criar uma classe (pequeno) para encapsular essas informações. Se você criar um RecordType classe, então ele pode ter funções como:

vazio setDeleted ();

vazio clearDeleted ();

bool isDeleted ();

etc ... (ou o que ternos de convenções)

Ele poderia validar combinações (no caso em que nem todas as combinações são legais, por exemplo, se 'novo' e 'excluídos' não pôde ser definidos ao mesmo tempo). Se você acabou de usar máscaras de bits etc, em seguida, o código que define as necessidades do estado para validar, uma classe pode encapsular essa lógica também.

A classe também pode dar-lhe a capacidade de anexar significativa informações de registro para cada estado, você pode adicionar uma função para retornar uma representação de string do estado atual etc (ou usar os operadores streaming '<<').

Por tudo isso, se você está preocupado com armazenamento que você ainda pode ter a classe só tem um 'char' membro de dados, de modo que só pode tomar uma pequena quantidade de armazenamento (supondo que ele é não virtual). É claro que, dependendo do hardware etc você pode ter problemas de alinhamento.

Você pode ter os valores de bits reais não são visíveis para o resto do 'mundo' se eles estão em um namespace anônimo dentro do arquivo CPP em vez de no cabeçalho do arquivo.

Se você achar que o código usando o enum / # definir / máscara de bits etc tem um monte de código de 'apoio' para lidar com combinações inválidas, registrando etc, em seguida, encapsulamento em uma classe pode valer a pena considerar. É claro que na maioria das vezes problemas simples é melhor fora com soluções simples ...

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top