Por que atribuir um valor a um campo de bit não está retornando o mesmo valor?


96

Eu vi o código abaixo nesta postagem do Quora :

#include <stdio.h>

struct mystruct { int enabled:1; };
int main()
{
  struct mystruct s;
  s.enabled = 1;
  if(s.enabled == 1)
    printf("Is enabled\n"); // --> we think this to be printed
  else
    printf("Is disabled !!\n");
}

Em C e C ++, a saída do código é inesperada ,

Está desabilitado !!

Embora a explicação relacionada ao "bit de sinal" seja dada nessa postagem, não consigo entender como é possível que definamos algo e então isso não reflita como está.

Alguém pode dar uma explicação mais elaborada?


Nota : Ambas as tags E são necessários, porque seus padrões diferem ligeiramente para descrever os campos de bits. Veja as respostas para especificações C e especificações C ++ .


46
Como o campo de bits é declarado como inteu acho que ele só pode conter os valores 0e -1.
Osiris

6
pense em como int armazena -1. Todos os bits são definidos como 1. Portanto, se você tiver apenas um bit, ele deve ser claramente -1. Portanto, 1 e -1 no int de 1 bit são iguais. Altere a verificação para 'if (s.enabled! = 0)' e funciona. Porque 0 não pode ser.
Jürgen

3
É verdade que essas regras são as mesmas em C e C ++. Mas, de acordo com as políticas de uso de tag , devemos apenas marcar isso como C e evitar a marcação cruzada quando não for necessário. Vou remover a parte C ++, isso não deve afetar as respostas postadas.
Lundin

8
Você já tentou mudar para struct mystruct { unsigned int enabled:1; };?
ChatterOne

4
Por favor, leia as políticas de tag C e C ++ , particularmente a parte relacionada à marcação cruzada de C e C ++, estabelecidas por meio de consenso da comunidade aqui . Não vou entrar em uma guerra de reversão, mas esta questão está incorretamente marcada como C ++. Mesmo que as linguagens tenham alguma ligeira diferença por causa de vários TCs, faça uma pergunta separada sobre a diferença entre C e C ++.
Lundin

Respostas:


77

Os campos de bits são incrivelmente mal definidos pelo padrão. Dado este código struct mystruct {int enabled:1;};, então não sabemos:

  • Quanto espaço isso ocupa - se há bits / bytes de preenchimento e onde eles estão localizados na memória.
  • Onde o bit está localizado na memória. Não definido e também depende de endianess.
  • Se um campo de int:nbits deve ser considerado com ou sem sinal.

Em relação à última parte, C17 6.7.2.1/10 diz:

Um campo de bits é interpretado como tendo um tipo inteiro assinado ou não assinado, consistindo no número especificado de bits 125)

Nota não normativa explicando o acima:

125) Conforme especificado em 6.7.2 acima, se o especificador de tipo real usado é intou um nome de typedef definido como int, então ele é definido pela implementação se o campo de bits é assinado ou não.

No caso de o campo de bits ser considerado como signed int bits e você fazer um bit de tamanho 1, então não há espaço para dados, apenas para o bit de sinal. Esta é a razão pela qual seu programa pode dar resultados estranhos em alguns compiladores.

Boa prática:

  • Nunca use campos de bits para nenhum propósito.
  • Evite usar inttipo assinado para qualquer forma de manipulação de bits.

5
No trabalho, temos static_asserts no tamanho e endereço de bitfields apenas para garantir que eles não sejam preenchidos. Usamos bitfields para registros de hardware em nosso firmware.
Michael

4
@Lundin: A coisa feia com as máscaras e deslocamentos # define-d é que seu código fica repleto de deslocamentos e operadores AND / OR bit a bit. Com bitfields, o compilador cuida disso para você.
Michael

4
@Michael Com bitfields, o compilador cuida disso para você. Bem, tudo bem se seus padrões para "cuidar disso" forem "não portáteis" e "imprevisíveis". Os meus são mais altos do que isso.
Andrew Henle

3
@AndrewHenle Leushenko está dizendo que, da perspectiva apenas do próprio padrão C , cabe à implementação decidir se ela segue ou não o ABI x86-64.
mtraceur

3
@AndrewHenle Certo, concordo em ambos os pontos. Meu ponto é que acho que sua discordância com Leushenko se resume ao fato de que você está usando "implementação definida" para se referir apenas a coisas nem estritamente definidas pelo padrão C nem estritamente definidas pela plataforma ABI, e ele está usando isso para se referir a qualquer coisa que não seja estritamente definida apenas pelo padrão C.
mtraceur

58

Não consigo entender como é possível que definimos algo e então não apareça como está.

Você está perguntando por que ele compila vs. apresenta um erro?

Sim, o ideal é que ele forneça um erro. E faz, se você usar os avisos do seu compilador. No GCC, com -Werror -Wall -pedantic:

main.cpp: In function 'int main()':
main.cpp:7:15: error: overflow in conversion from 'int' to 'signed char:1' 
changes value from '1' to '-1' [-Werror=overflow]
   s.enabled = 1;
           ^

O motivo pelo qual isso foi deixado para ser definido pela implementação versus um erro pode ter mais a ver com usos históricos, onde exigir um elenco significaria quebrar o código antigo. Os autores da norma podem acreditar que os avisos foram suficientes para compensar os interessados.

Para acrescentar um pouco de prescritivismo, vou ecoar a declaração de @Lundin: "Nunca use campos de bits para nenhum propósito." Se você tiver bons motivos para ser baixo e específico sobre os detalhes do layout de sua memória, que o levariam a pensar que você precisava de bitfields em primeiro lugar, os outros requisitos associados que você quase certamente terá irão se chocar contra sua subespecificação.

(TL; DR - Se você é sofisticado o suficiente para "precisar" legitimamente de campos de bits, eles não estão bem definidos para atendê-lo.)


15
Os autores do padrão estavam de férias no dia em que o capítulo do campo de bits foi projetado. Então o zelador tinha que fazer isso. Não há lógica sobre qualquer coisa a respeito de como bit-campos são projetados.
Lundin

9
Não há nenhum fundamento técnico coerente . Mas isso me leva a concluir que havia uma razão política : evitar tornar incorreto qualquer código ou implementação existente. Mas o resultado é que há muito pouco sobre os campos de bits em que você possa confiar.
John Bollinger

6
@JohnBollinger Definitivamente havia política em vigor, que causou muitos danos ao C90. Certa vez, conversei com um membro do comitê que explicou a origem de muitas porcarias - o padrão ISO não podia favorecer certas tecnologias existentes. É por isso que estamos presos a coisas idiotas como suporte para complemento de 1 e magnitude do sinal char, sinalização definida pela implementação de , suporte para bytes que não são de 8 bits, etc. etc. Eles não podiam dar aos computadores idiotas uma desvantagem de mercado.
Lundin

1
@Lundin Seria interessante ver uma coleção de artigos e autópsias de pessoas que acreditavam que as trocas foram feitas por engano e por quê. Eu me pergunto quanto estudo desses "nós fizemos isso da última vez e deu / não deu certo" se tornou conhecimento institucional para informar o próximo caso, em vez de apenas histórias na cabeça das pessoas.
HostileFork diz não confie em SE

1
Isso ainda está listado como ponto não. 1 dos princípios originais de C na Carta C2x: "O código existente é importante, as implementações existentes não." ... "nenhuma implementação foi considerada o exemplo pelo qual definir C: Presume-se que todas as implementações existentes devem mudar um pouco para estar em conformidade com o Padrão."
Leushenko

23

Este é o comportamento definido pela implementação. Estou fazendo a suposição de que as máquinas em que você está executando isso usam intinteiros com sinal de dois cumprimentos e tratam neste caso como um inteiro com sinal para explicar por que você não insere a parte if true da instrução if.

struct mystruct { int enabled:1; };

declara enablecomo um campo de 1 bit. Como está assinado, os valores válidos são -1e 0. Definindo o campo para 1transbordar aquele bit voltando para-1 (este é um comportamento indefinido)

Essencialmente, ao lidar com um campo de bits com sinal, o valor máximo é o 2^(bits - 1) - 1que é 0neste caso.


"uma vez que é assinado, os valores válidos são -1 e 0". Quem disse que está assinado? Não é um comportamento definido, mas sim definido pela implementação. Se estiver assinado, os valores válidos são -e +. O complemento de 2 não importa.
Lundin

5
@Lundin Um número de complemento duplo de 1 bit possui apenas dois valores possíveis. Se o bit estiver definido, como é o bit de sinal, é -1. Se não estiver definido, então é "positivo" 0. Eu sei que esta é a implementação definida, estou apenas explicando os resultados usando a implantação mais comum
NathanOliver

1
A chave aqui é que o complemento de 2 ou qualquer outra forma assinada não pode funcionar com um único bit disponível.
Lundin

1
@JohnBollinger Eu entendo isso. É por isso que tenho o discliamer de que esta é a implementação definida. Pelo menos para os 3 grandes, todos eles tratam intcomo assinado neste caso. É uma pena que os campos de bits estejam tão subespecificados. Basicamente está aqui este recurso, consulte seu compilador para saber como usá-lo.
NathanOliver

1
@Lundin, a formulação do padrão para a representação de inteiros com sinal pode perfeitamente lidar bem com o caso em que há bits de valor zero, pelo menos em duas das três alternativas permitidas. Isso funciona porque atribui valores de posição (negativos) aos bits de sinal, em vez de dar a eles uma interpretação algorítmica.
John Bollinger

10

Você pode pensar nisso como no sistema de complemento de 2, o bit mais à esquerda é o bit do sinal. Qualquer número inteiro com sinal com o conjunto de bits mais à esquerda é, portanto, um valor negativo.

Se você tiver um inteiro assinado de 1 bit, ele terá apenas o bit de sinal. Então, atribuindo1 a esse bit único pode definir apenas o bit de sinal. Portanto, ao lê-lo novamente, o valor é interpretado como negativo e, portanto, -1.

Os valores que um inteiro com sinal de 1 bit pode conter são -2^(n-1)= -2^(1-1)= -2^0= -1e2^n-1= 2^1-1=0


8

De acordo com o padrão C ++ n4713 , um trecho de código muito semelhante é fornecido. O tipo usado é BOOL(personalizado), mas pode se aplicar a qualquer tipo.

12.2.4

4 Se o valor true ou false for armazenado em um campo de bitsboolde qualquer tamanho (incluindo um campo de bits de um bit), oboolvalororiginale o valor do campo de bits devem ser comparados. Se o valor de um enumerador for armazenado em um campo de bits do mesmo tipo de enumeração e o número de bits no campo de bits for grande o suficiente para conter todos os valores desse tipo de enumeração (10.2), o valor do enumerador original e o o valor do campo de bits deve ser igual . [Exemplo:

enum BOOL { FALSE=0, TRUE=1 };
struct A {
  BOOL b:1;
};
A a;
void f() {
  a.b = TRUE;
  if (a.b == TRUE)    // yields true
    { /* ... */ }
}

- exemplo final]


À primeira vista, a parte em negrito parece aberta para interpretação. No entanto, a intenção correta torna-se clara quando o enum BOOLé derivado do int.

enum BOOL : int { FALSE=0, TRUE=1 }; // ***this line
struct mystruct { BOOL enabled:1; };
int main()
{
  struct mystruct s;
  s.enabled = TRUE;
  if(s.enabled == TRUE)
    printf("Is enabled\n"); // --> we think this to be printed
  else
    printf("Is disabled !!\n");
}

Com o código acima, ele dá um aviso sem -Wall -pedantic:

aviso: 'mystruct :: enabled' é muito pequeno para conter todos os valores de 'enum BOOL' struct mystruct { BOOL enabled:1; };

O resultado é:

Está desabilitado !! (ao usar enum BOOL : int)

Se enum BOOL : intfor simplificado enum BOOL, a saída será como a passagem padrão acima especifica:

Está habilitado (ao usar enum BOOL)


Portanto, pode-se concluir, também como poucas outras respostas, que o inttipo não é grande o suficiente para armazenar o valor "1" em um único campo de bits.


0

Não há nada de errado com sua compreensão de bitfields que eu possa ver. O que vejo é que você redefiniu mystruct primeiro como struct mystruct {int enabled: 1; } e então como struct mystruct s; . O que você deveria ter codificado era:

#include <stdio.h>

struct mystruct { int enabled:1; };
int main()
{
    mystruct s; <-- Get rid of "struct" type declaration
    s.enabled = 1;
    if(s.enabled == 1)
        printf("Is enabled\n"); // --> we think this to be printed
    else
        printf("Is disabled !!\n");
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.