¿Por qué la asignación de un valor a un campo de bits no devuelve el mismo valor?

96

Vi el siguiente código en esta publicación de 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");
}

Tanto en C como en C ++, la salida del código es inesperada ,

Está desactivado !!

Aunque la explicación relacionada con el "bit de signo" se da en esa publicación, no puedo entender cómo es posible que establezcamos algo y luego no se refleje como está.

¿Alguien puede dar una explicación más elaborada?


Nota : ambas etiquetas Y son necesarios, porque sus estándares difieren ligeramente para describir los campos de bits. Consulte las respuestas para la especificación C y la especificación C ++ .

iammilind
fuente
46
Dado que el campo de bits se declara, intcreo que solo puede contener los valores 0y -1.
Osiris
6
piense en cómo int almacena -1. Todos los bits se establecen en 1. Por lo tanto, si solo tiene un bit, claramente tiene que ser -1. Entonces 1 y -1 en el int de 1 bit son lo mismo. Cambie la marca de verificación a 'if (s.enabled! = 0)' y funciona. Porque 0 no puede ser.
Jürgen
3
Es cierto que estas reglas son las mismas en C y C ++. Pero de acuerdo con las políticas de uso de etiquetas , solo debemos etiquetar esto como C y abstenernos de etiquetado cruzado cuando no sea necesario. Eliminaré la parte de C ++, no debería afectar ninguna respuesta publicada.
Lundin
8
¿Has intentado cambiarlo a struct mystruct { unsigned int enabled:1; };?
ChatterOne
4
Por favor lea las políticas de etiquetas de C y C ++ , particularmente la parte relacionada con el etiquetado cruzado de C y C ++, establecidas a través del consenso de la comunidad aquí . No voy a entrar en una guerra de reversión, pero esta pregunta está etiquetada incorrectamente como C ++. Incluso si los lenguajes tienen alguna pequeña diferencia debido a varios TC, haga una pregunta separada sobre la diferencia entre C y C ++.
Lundin

Respuestas:

78

Los campos de bits están increíblemente mal definidos por el estándar. Dado este código struct mystruct {int enabled:1;};, entonces no sabemos:

  • Cuánto espacio ocupa, si hay bits / bytes de relleno y dónde se encuentran en la memoria.
  • Dónde se encuentra el bit en la memoria. No se define y también depende de la endiabilidad.
  • Si un int:ncampo de bits debe considerarse firmado o no firmado.

Con respecto a la última parte, C17 6.7.2.1/10 dice:

Un campo de bits se interpreta como un tipo de entero con o sin signo que consta del número especificado de bits ( 125)

Nota no normativa que explica lo anterior:

125) Como se especifica en 6.7.2 anterior, si el especificador de tipo real utilizado es into un nombre de definición de tipo definido como int, entonces está definido por la implementación si el campo de bits está firmado o sin firmar.

En caso de que se considere el campo de bits signed inty se haga un poco de tamaño 1, entonces no hay espacio para los datos, solo para el bit de signo. Esta es la razón por la que su programa puede dar resultados extraños en algunos compiladores.

Buena práctica:

  • Nunca use campos de bits para ningún propósito.
  • Evite el uso de caracteres con signo intpara cualquier forma de manipulación de bits.
Lundin
fuente
5
En el trabajo, tenemos static_asserts sobre el tamaño y la dirección de los campos de bits solo para asegurarnos de que no estén rellenos. Usamos campos de bits para registros de hardware en nuestro firmware.
Michael
4
@Lundin: Lo feo con las máscaras y compensaciones # define-d es que su código se llena de cambios y operadores AND / OR bit a bit. Con bitfields, el compilador se encarga de eso por usted.
Michael
4
@Michael Con bitfields, el compilador se encarga de eso por usted. Bueno, está bien si sus estándares para "se encarga de eso" son "no portátiles" e "impredecibles". Los míos son más altos que eso.
Andrew Henle
3
@AndrewHenle Leushenko está diciendo que desde la perspectiva del estándar C en sí mismo , depende de la implementación si elige o no seguir la ABI x86-64 o no.
mtraceur
3
@AndrewHenle Correcto, estoy de acuerdo en ambos puntos. Mi punto fue que creo que su desacuerdo con Leushenko se reduce al hecho de que está usando "implementación definida" para referirse solo a cosas que no están estrictamente definidas por el estándar C ni estrictamente definidas por la plataforma ABI, y lo está usando para referirse a cualquier cosa que no esté estrictamente definida por el estándar C.
mtraceur
58

No puedo entender, cómo es posible que establezcamos algo y luego no se muestre como está.

¿Estás preguntando por qué se compila y te da un error?

Sí, idealmente debería darte un error. Y lo hace, si usa las advertencias de su compilador. En GCC, con -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;
           ^

El razonamiento de por qué esto se deja en manos de una implementación definida frente a un error puede tener más que ver con los usos históricos, donde requerir una conversión significaría romper el código antiguo. Los autores de la norma pueden creer que las advertencias fueron suficientes para compensar a los interesados.

Para agregar algo de prescriptivismo, me haré eco de la declaración de @ Lundin: "Nunca uses campos de bits para ningún propósito". Si tiene el tipo de buenas razones para ser de bajo nivel y específico sobre los detalles de diseño de su memoria que lo llevarían a pensar que necesitaba campos de bits en primer lugar, los otros requisitos asociados que tiene casi con certeza se encontrarán con su subespecificación.

(TL; DR: si es lo suficientemente sofisticado como para "necesitar" campos de bits legítimamente, no están lo suficientemente bien definidos para servirle).

HostileFork dice que no confíes en SE
fuente
15
Los autores de la norma estaban de vacaciones el día en que se diseñó el capítulo de campo de bits. Entonces el conserje tuvo que hacerlo. No hay ninguna justificación sobre todo con respecto a cómo los campos de bits están diseñados.
Lundin
9
No hay una justificación técnica coherente . Pero eso me lleva a concluir que había una justificación política : evitar que el código o las implementaciones existentes fueran incorrectos. Pero el resultado es que hay muy pocos campos de bits en los que puede confiar.
John Bollinger
6
@JohnBollinger Definitivamente hubo política en su lugar, eso causó mucho daño a C90. Una vez hablé con un miembro del comité que me explicó la fuente de mucha basura: no se podía permitir que la norma ISO favoreciera ciertas tecnologías existentes. Es por eso que estamos atascados con cosas estúpidas como soporte para el complemento de 1 y magnitud con signo, firma definida por la implementación de char, soporte para bytes que no son de 8 bits, etc. No se les permitió darles a las computadoras idiotas una desventaja de mercado.
Lundin
1
@Lundin Sería interesante ver una colección de reseñas y autopsias de personas que creían que las compensaciones se habían cometido por error y por qué. Me pregunto cuánto estudio de estos "hicimos la última vez, y funcionó / ​​no funcionó" se ha convertido en conocimiento institucional para informar el próximo caso de este tipo, versus solo historias en la cabeza de la gente.
HostileFork dice que no confíes en SE el
1
Esto todavía figura como punto no. 1 de los principios originales de C en la Carta C2x: "El código existente es importante, las implementaciones existentes no lo son". ... "ninguna implementación se consideró el ejemplo para definir C: se supone que todas las implementaciones existentes deben cambiar algo para ajustarse a la Norma".
Leushenko
23

Este es el comportamiento definido por la implementación. Estoy asumiendo que las máquinas en las que está ejecutando esto usan dos enteros con signo complementario y se tratan inten este caso como un entero con signo para explicar por qué no ingresa si es verdadero como parte de la declaración if.

struct mystruct { int enabled:1; };

declara enablecomo un campo de bits de 1 bit. Dado que está firmado, los valores válidos son -1y 0. Establecer el campo en 1desbordamientos a los que se remonta el bit -1(este es un comportamiento indefinido)

Básicamente, cuando se trata de un campo de bits con signo, el valor máximo es el 2^(bits - 1) - 1que es 0en este caso.

NathanOliver
fuente
"dado que está firmado, los valores válidos son -1 y 0". ¿Quién dijo que está firmado? No es un comportamiento definido, sino definido por la implementación. Si está firmado, los valores válidos son -y +. El complemento de 2 no importa.
Lundin
5
@Lundin Un número de cumplido de 1 bit dos solo tiene dos valores posibles. Si el bit está establecido, dado que es el bit de signo, es -1. Si no está configurado, entonces es "positivo" 0. Sé que esto está definido por la implementación, solo estoy explicando los resultados utilizando la implantación más común
NathanOliver
1
La clave aquí es más bien que el complemento a 2 o cualquier otra forma firmada no puede funcionar con un solo bit disponible.
Lundin
1
@JohnBollinger Lo entiendo. Es por eso que tengo el discliamer de que esto es implementación definida. Al menos para los 3 grandes, todos tratan intcomo firmados en este caso. Es una pena que los campos de bits estén tan subespecificados. Básicamente, aquí está esta característica, consulte a su compilador sobre cómo usarla.
NathanOliver
1
@Lundin, la redacción del estándar para la representación de enteros con signo puede manejar perfectamente bien el caso en el que hay bits de valor cero, al menos en dos de las tres alternativas permitidas. Esto funciona porque asigna valores posicionales (negativos) a los bits de signo, en lugar de darles una interpretación algorítmica.
John Bollinger
10

Puede pensar en ello como que en el sistema de complemento a 2, el bit más a la izquierda es el bit de signo. Cualquier entero con signo con el conjunto de bits más a la izquierda es, por tanto, un valor negativo.

Si tiene un entero con signo de 1 bit, solo tiene el bit de signo. Así que la asignación 1a ese único bit solo puede establecer el bit de signo. Entonces, al leerlo de nuevo, el valor se interpreta como negativo y también es -1.

Los valores que puede contener un entero de 1 bit con signo son -2^(n-1)= -2^(1-1)= -2^0= -1y2^n-1= 2^1-1=0

Paul Ogilvie
fuente
8

Según el estándar n4713 de C ++, se proporciona un fragmento de código muy similar. El tipo utilizado es BOOL(personalizado), pero puede aplicarse a cualquier tipo.

12.2.4

4 Si el valor verdadero o falso se almacena en un campo de bits de tipoboolde cualquier tamaño (incluido un campo de bits de un bit), elboolvalororiginaly el valor del campo de bits se compararán iguales. Si el valor de un enumerador se almacena en un campo de bits del mismo tipo de enumeración y el número de bits en el campo de bits es lo suficientemente grande para contener todos los valores de ese tipo de enumeración (10.2), el valor del enumerador original y el el valor del campo de bits se comparará igual . [Ejemplo:

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

- ejemplo final]


A primera vista, la parte en negrita parece abierta a la interpretación. Sin embargo, la intención correcta se vuelve clara cuando enum BOOLse deriva de 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");
}

Con el código anterior, da una advertencia sin -Wall -pedantic:

advertencia: 'mystruct :: enabled' es demasiado pequeño para contener todos los valores de 'enum BOOL' struct mystruct { BOOL enabled:1; };

La salida es:

Está desactivado !! (cuando se usa enum BOOL : int)

Si enum BOOL : intse simplifica enum BOOL, la salida es como especifica el pasaje estándar anterior:

Está habilitado (cuando se usa enum BOOL)


Por lo tanto, se puede concluir, también como pocas otras respuestas tienen, que el inttipo no es lo suficientemente grande para almacenar el valor "1" en un solo campo de bits de bits.

iammilind
fuente
0

No hay nada de malo en su comprensión de los campos de bits que yo pueda ver. Lo que veo es que redefinió mystruct primero como struct mystruct {int habilitado: 1; } y luego como struct mystruct s; . Lo que debería haber codificado fue:

#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");
}
ar18
fuente