Pourquoi attribuer une valeur à un champ de bits ne renvoie-t-il pas la même valeur?

96

J'ai vu le code ci-dessous dans cet article 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");
}

En C et C ++, la sortie du code est inattendue ,

Est désactivé !!

Bien que l'explication relative au "bit de signe" soit donnée dans ce post, je suis incapable de comprendre comment il est possible que nous définissions quelque chose et que cela ne reflète pas tel quel.

Quelqu'un peut-il donner une explication plus élaborée?


Remarque : les deux balises & sont nécessaires, car leurs normes diffèrent légèrement pour décrire les champs de bits. Voir les réponses pour la spécification C et la spécification C ++ .

iammilind
la source
46
Étant donné que le champ de bits est déclaré car intje pense qu'il ne peut contenir que les valeurs 0et -1.
Osiris
6
il suffit de penser à la façon dont int stocke -1. Tous les bits sont mis à 1. Par conséquent, si vous n'avez qu'un seul bit, il doit clairement être -1. Donc 1 et -1 dans le 1 bit int sont identiques. Changez la vérification en «if (s.enabled! = 0)» et cela fonctionne. Parce que 0 ça ne peut pas être.
Jürgen
3
Il est vrai que ces règles sont les mêmes en C et C ++. Mais selon les politiques d' utilisation des balises , nous ne devrions marquer cela que comme C et nous abstenir de faire des balises croisées lorsque cela n'est pas nécessaire. Je vais supprimer la partie C ++, cela ne devrait pas affecter les réponses publiées.
Lundin
8
Avez-vous essayé de le changer en struct mystruct { unsigned int enabled:1; };?
ChatterOne
4
Veuillez lire les politiques de balises C et C ++ , en particulier la partie concernant le balisage croisé C et C ++ à la fois, établie par consensus de la communauté ici . Je ne vais pas dans une guerre de restauration, mais cette question est incorrectement étiquetée C ++. Même si les langages présentent une légère différence à cause de divers TC, posez une question distincte sur la différence entre C et C ++.
Lundin

Réponses:

78

Les champs de bits sont incroyablement mal définis par la norme. Compte tenu de ce code struct mystruct {int enabled:1;};, alors nous ne savons pas :

  • Combien d'espace cela occupe - s'il y a des bits / octets de remplissage et où ils se trouvent dans la mémoire.
  • Où le bit est situé en mémoire. Non défini et dépend également de l'endianess.
  • int:nIndique si un champ de bits doit être considéré comme signé ou non signé.

Concernant la dernière partie, C17 6.7.2.1/10 dit:

Un champ de bits est interprété comme ayant un type entier signé ou non signé constitué du nombre spécifié de bits 125)

Note non normative expliquant ce qui précède:

125) Comme spécifié au 6.7.2 ci-dessus, si le spécificateur de type réel utilisé est intou un typedef-name défini comme int, alors il est défini par l'implémentation si le champ de bits est signé ou non signé.

Dans le cas où le champ de signed intbits doit être considéré comme et que vous faites un peu de taille 1, alors il n'y a pas de place pour les données, uniquement pour le bit de signe. C'est la raison pour laquelle votre programme peut donner des résultats étranges sur certains compilateurs.

Bonnes pratiques:

  • N'utilisez jamais de champs de bits pour quelque raison que ce soit.
  • Évitez d'utiliser le inttype signé pour toute forme de manipulation de bits.
Lundin
la source
5
Au travail, nous avons des static_asserts sur la taille et l'adresse des champs de bits juste pour nous assurer qu'ils ne sont pas remplis. Nous utilisons des champs de bits pour les registres matériels de notre firmware.
Michael
4
@Lundin: La chose laide avec les masques et les décalages # define-d est que votre code est jonché de décalages et d'opérateurs ET / OU bit par bit. Avec les champs de bits, le compilateur s'en charge pour vous.
Michael
4
@Michael Avec les bitfields, le compilateur s'en charge pour vous. Eh bien, ce n'est pas grave si vos normes pour «s'occupe de cela» sont «non portables» et «imprévisibles». Les miens sont plus élevés que ça.
Andrew Henle
3
@AndrewHenle Leushenko dit que du point de vue du standard C lui-même , il appartient à l'implémentation de choisir ou non de suivre l'ABI x86-64 ou non.
mtraceur
3
@AndrewHenle Oui, je suis d'accord sur les deux points. Mon point était que je pense que votre désaccord avec Leushenko se résume au fait que vous utilisez "implémentation définie" pour faire référence uniquement à des choses qui ne sont ni strictement définies par le standard C ni strictement définies par la plate-forme ABI, et il l'utilise pour faire référence à tout ce qui n'est pas strictement défini par le standard C.
mtraceur
58

Je suis incapable de comprendre, comment est-il possible que nous définissions quelque chose et que cela n'apparaisse pas tel quel.

Vous demandez-vous pourquoi il compile et vous donne une erreur?

Oui, cela devrait idéalement vous donner une erreur. Et c'est le cas, si vous utilisez les avertissements de votre compilateur. Dans GCC, avec -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;
           ^

La raison pour laquelle cela est laissé à la définition de l'implémentation par rapport à une erreur peut avoir plus à voir avec les utilisations historiques, où exiger un cast signifierait casser l'ancien code. Les auteurs de la norme peuvent penser que les avertissements ont suffi à combler les lacunes des personnes concernées.

Pour ajouter un peu de prescriptivisme, je ferai écho à la déclaration de @ Lundin: "N'utilisez jamais de champs de bits pour quelque raison que ce soit." Si vous avez le genre de bonnes raisons d'obtenir des détails de bas niveau et spécifiques sur la disposition de votre mémoire qui vous amèneraient à penser que vous aviez besoin de champs de bits en premier lieu, les autres exigences associées que vous avez presque certainement se heurteront à leur sous-spécification.

(TL; DR - Si vous êtes assez sophistiqué pour légitimement «avoir besoin» de champs de bits, ils ne sont pas assez bien définis pour vous servir.)

HostileFork dit de ne pas faire confiance à SE
la source
15
Les auteurs de la norme étaient en vacances le jour où le chapitre sur les champs de bits a été conçu. Le concierge devait donc le faire. Il n'y a aucune raison d'être à propos de quoi que ce soit sur la façon dont les champs de bits sont conçus.
Lundin
9
Il n'y a pas de justification technique cohérente . Mais cela m'amène à conclure qu'il y avait une justification politique : éviter de rendre le code existant ou les implémentations incorrects. Mais le résultat est qu'il y a très peu de champs de bits sur lesquels vous pouvez compter.
John Bollinger
6
@JohnBollinger Il y avait définitivement de la politique en place, qui a causé beaucoup de dégâts au C90. J'ai parlé une fois avec un membre du comité qui a expliqué la source de beaucoup de merde - la norme ISO ne pouvait pas être autorisée à favoriser certaines technologies existantes. C'est pourquoi nous sommes coincés avec des choses idiotes comme la prise en charge du complément de 1 et de la magnitude signée, la signature définie par l'implémentation de char, la prise en charge des octets qui ne sont pas 8 bits, etc.
Lundin
1
@Lundin Il serait intéressant de voir une collection d'écrits et de post-mortems de personnes qui croyaient que des compromis avaient été faits par erreur, et pourquoi. Je me demande dans quelle mesure l'étude de ces «nous avons fait cela la dernière fois, et cela a fonctionné / n'a pas fonctionné» est devenue une connaissance institutionnelle pour informer le prochain cas de ce genre, par opposition à des histoires dans la tête des gens.
HostileFork dit de ne pas faire confiance au SE
1
Ceci est toujours répertorié comme point no. 1 des principes originaux de C dans la Charte C2x: "Le code existant est important, les implémentations existantes ne le sont pas." ... "aucune implémentation n'a été présentée comme l'exemple par lequel définir C: il est supposé que toutes les implémentations existantes doivent quelque peu changer pour se conformer à la norme."
Leushenko
23

Il s'agit d'un comportement défini par l'implémentation. Je suppose que les machines sur lesquelles vous exécutez ceci utilisent des entiers signés à complément double et les traitent intdans ce cas comme un entier signé pour expliquer pourquoi vous n'entrez pas si une partie vraie de l'instruction if.

struct mystruct { int enabled:1; };

déclare enablecomme un champ de bits de 1 bit. Puisqu'il est signé, les valeurs valides sont -1et 0. Définir le champ sur les 1débordements de ce bit -1(il s'agit d'un comportement non défini)

Essentiellement, lorsqu'il s'agit d'un champ de bits signé, la valeur maximale est 2^(bits - 1) - 1ce qui est 0dans ce cas.

NathanOliver
la source
"une fois qu'il est signé, les valeurs valides sont -1 et 0". Qui a dit qu'il était signé? Ce n'est pas un comportement défini mais défini par l'implémentation. S'il est signé, les valeurs valides sont -et +. Le complément de 2 n'a pas d'importance.
Lundin
5
@Lundin Un numéro de complément à deux bits de 1 bit n'a que deux valeurs possibles. Si le bit est mis à 1, puisqu'il s'agit du bit de signe, il vaut -1. S'il n'est pas défini, alors il est "positif" 0. Je sais que c'est l'implémentation définie,
j'explique
1
La clé ici est plutôt que le complément de 2 ou toute autre forme signée ne peut pas fonctionner avec un seul bit disponible.
Lundin
1
@JohnBollinger Je comprends cela. C'est pourquoi j'ai la divulgation que c'est la mise en œuvre définie. Au moins pour les 3 grands, ils traitent tous intcomme signés dans ce cas. Il est dommage que les champs de bits soient si sous-spécifiés. C'est essentiellement ici est cette fonctionnalité, consultez votre compilateur pour savoir comment l'utiliser.
NathanOliver
1
@Lundin, la formulation standard pour la représentation des entiers signés peut parfaitement gérer le cas où il y a des bits de valeur nulle, au moins dans deux des trois alternatives autorisées. Cela fonctionne car il attribue des valeurs de position (négatives) aux bits de signe, plutôt que de leur donner une interprétation algorithmique.
John Bollinger
10

Vous pourriez penser que dans le système de complément à 2, le bit le plus à gauche est le bit de signe. Tout entier signé avec le bit le plus à gauche est donc une valeur négative.

Si vous avez un entier signé de 1 bit, il n'a que le bit de signe. L'attribution 1de ce bit unique ne peut donc définir que le bit de signe. Ainsi, lors de sa relecture, la valeur est interprétée comme négative, tout comme -1.

Les valeurs qu'un entier signé de 1 bit peut contenir sont -2^(n-1)= -2^(1-1)= -2^0= -1et2^n-1= 2^1-1=0

Paul Ogilvie
la source
8

Conformément à la norme C ++ n4713 , un extrait de code très similaire est fourni. Le type utilisé est BOOL(personnalisé), mais il peut s'appliquer à n'importe quel type.

12.2.4

4 Si la valeur true ou false est stockée dans un champ de bits de typeboolde n'importe quelle taille (y compris un champ de bits d'un bit), laboolvaleur d'origineet la valeur du champ de bits doivent être comparées égales. Si la valeur d'un énumérateur est stockée dans un champ de bits du même type d'énumération et que le nombre de bits dans le champ de bits est suffisamment grand pour contenir toutes les valeurs de ce type d'énumération (10.2), la valeur de l'énumérateur d'origine et le la valeur du champ de bits doit être comparée égale . [ Exemple:

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

- fin d'exemple]


Au premier coup d'œil, la partie en gras semble ouverte à l'interprétation. Cependant, l'intention correcte devient claire lorsque le enum BOOLest dérivé du 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");
}

Avec le code ci-dessus, il donne un avertissement sans -Wall -pedantic:

avertissement: 'mystruct :: enabled' est trop petit pour contenir toutes les valeurs de 'enum BOOL' struct mystruct { BOOL enabled:1; };

La sortie est:

Est désactivé !! (lors de l'utilisation enum BOOL : int)

Si enum BOOL : intest simplifié enum BOOL, alors la sortie est comme le spécifie le pasage standard ci-dessus:

Est activé (lors de l'utilisation enum BOOL)


Par conséquent, il peut être conclu, comme peu d'autres réponses l'ont fait, que le inttype n'est pas assez grand pour stocker la valeur "1" dans un seul champ de bits.

iammilind
la source
0

Il n'y a rien de mal dans votre compréhension des champs de bits que je peux voir. Ce que je vois, c'est que vous avez redéfini mystruct d'abord comme struct mystruct {int enabled: 1; } puis comme struct mystruct s; . Ce que vous auriez dû coder était:

#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
la source