web-dev-qa-db-fra.com

Mettre des bits supplémentaires dans un booléen rend vrai et faux en même temps

Si j'obtiens une variable bool et que je mets son deuxième bit à 1, alors la variable est évaluée à vrai et faux en même temps. Compilez le code suivant avec gcc6.3 avec -g option, (gcc-v6.3.0/Linux/RHEL6.0-2016-x86_64/bin/g++ -g main.cpp -o mytest_d) et exécutez l'exécutable. Vous obtenez ce qui suit.

Comment T peut-il être à la fois vrai et faux?

       value   bits 
       -----   ---- 
    T:   1     0001
after bit change
    T:   3     0011
T is true
T is false

Cela peut se produire lorsque vous appelez une fonction dans un langage différent (par exemple fortran) où la définition vraie et fausse est différente de C++. Pour fortran, si des bits ne sont pas 0, la valeur est vraie, si tous les bits sont nuls, la valeur est fausse.

#include <iostream>
#include <bitset>

using namespace std;

void set_bits_to_1(void* val){
  char *x = static_cast<char *>(val);

  for (int i = 0; i<2; i++ ){
    *x |= (1UL << i);
  }
}

int main(int argc,char *argv[])
{

  bool T = 3;

  cout <<"       value   bits " <<endl;
  cout <<"       -----   ---- " <<endl;
  cout <<"    T:   "<< T <<"     "<< bitset<4>(T)<<endl;

  set_bits_to_1(&T);


  bitset<4> bit_T = bitset<4>(T);
  cout <<"after bit change"<<endl;
  cout <<"    T:   "<< T <<"     "<< bit_T<<endl;

  if (T ){
    cout <<"T is true" <<endl;
  }

  if ( T == false){
    cout <<"T is false" <<endl;
  }


}

/////////////////////////////////// // Fonction Fortran qui n'est pas compatible avec C++ lorsqu'elle est compilée avec ifort.

       logical*1 function return_true()
         implicit none

         return_true = 1;

       end function return_true
41
BY408

En C++, la représentation binaire (et même la taille) d'un bool est définie par l'implémentation; il est généralement implémenté comme un type de taille char prenant 1 ou 0 comme valeurs possibles.

Si vous définissez sa valeur sur quelque chose de différent de ceux autorisés (dans ce cas spécifique en aliasant un bool par un char et en modifiant sa représentation binaire), vous enfreignez les règles du langage, donc tout peut arriver. En particulier, il est explicitement spécifié dans la norme qu'un bool "cassé" peut se comporter à la fois comme true et false (ou ni true ni false) en même temps:

L'utilisation d'une valeur bool de la manière décrite par la présente Norme internationale comme "non définie", par exemple en examinant la valeur d'un objet automatique non initialisé, pourrait le faire se comporter comme s'il n'était ni true ni false

(C++ 11, [basic.fundamental], note 47)


Dans ce cas particulier, vous pouvez voir comment cela a abouti dans cette situation bizarre : le premier if est compilé en

    movzx   eax, BYTE PTR [rbp-33]
    test    al, al
    je      .L22

qui charge T dans eax (avec l'extension zéro), et ignore l'impression si tout est nul; le prochain si est plutôt

    movzx   eax, BYTE PTR [rbp-33]
    xor     eax, 1
    test    al, al
    je      .L23

Le test if(T == false) est transformé en if(T^1), qui retourne juste le bit bas. Ce serait correct pour un bool valide, mais pour votre "cassé", il ne le coupe pas.

Notez que cette séquence bizarre n'est générée qu'à de faibles niveaux d'optimisation; à des niveaux plus élevés, cela se résume généralement à un contrôle zéro/non nul, et une séquence comme la vôtre est susceptible de devenir ne seule branche test/conditionnelle . Vous obtiendrez de toute façon un comportement bizarre dans d'autres contextes, par exemple lors de l'addition des valeurs de bool à d'autres entiers:

int foo(bool b, int i) {
    return i + b;
}

devient

foo(bool, int):
        movzx   edi, dil
        lea     eax, [rdi+rsi]
        ret

dil est "de confiance" pour être 0/1.


Si votre programme est entièrement en C++, alors la solution est simple: ne cassez pas les valeurs de bool de cette façon, évitez de jouer avec leur représentation binaire et tout ira bien; en particulier, même si vous affectez un entier à un bool le compilateur émettra le code nécessaire pour vous assurer que la valeur résultante est un bool valide, donc votre bool T = 3 est en effet sûr, et T se retrouvera avec un true dans ses tripes.

Si, au lieu de cela, vous devez interagir avec du code écrit dans d'autres langages qui ne partagent pas la même idée de ce qu'est un bool, évitez simplement bool pour le code "limite" et faites-en un marshalage approprié. entier de taille. Cela fonctionnera dans les conditionnels & co. tout aussi bien.


Mise à jour sur le côté Fortran/interopérabilité du problème

Clause de non-responsabilité tout ce que je sais de Fortran, c'est ce que j'ai lu ce matin sur des documents standard, et que j'ai des cartes perforées avec des listes Fortran que j'utilise comme signets, alors allez-y doucement avec moi.

Tout d'abord, ce genre de choses d'interopérabilité linguistique ne fait pas partie des normes linguistiques, mais de la plate-forme ABI. Comme nous parlons de Linux x86-64, le document pertinent est le System V x86-64 ABI .

Tout d'abord, il n'est spécifié nulle part que le type C _Bool (Qui est défini comme étant identique à C++ bool à la note 3.1.2 †) a une compatibilité avec Fortran LOGICAL; en particulier, au 9.2.2, le tableau 9.2 spécifie que "plain" LOGICAL est mappé sur signed int. À propos des types TYPE*N, Il est indiqué que

La notation "TYPE*N" Spécifie que les variables ou les agrégats de type TYPE doivent occuper N octets de stockage.

(ibid.)

Il n'y a pas de type équivalent explicitement spécifié pour LOGICAL*1, Et c'est compréhensible: ce n'est même pas standard; en effet, si vous essayez de compiler un programme Fortran contenant un LOGICAL*1 en mode compatible Fortran 95, vous obtenez des avertissements à ce sujet, à la fois par ifort

./example.f90(2): warning #6916: Fortran 95 does not allow this length specification.   [1]

    logical*1, intent(in) :: x

------------^

et par gfort

./example.f90:2:13:
     logical*1, intent(in) :: x
             1
Error: GNU Extension: Nonstandard type declaration LOGICAL*1 at (1)

donc les eaux sont déjà brouillées; donc, en combinant les deux règles ci-dessus, j'irais pour signed char pour être sûr.

Cependant: l'ABI précise également:

Les valeurs pour le type LOGICAL sont .TRUE. Implémentées comme 1 et .FALSE. Implémentées comme 0.

Donc, si vous avez un programme qui stocke autre chose que 1 et 0 dans une valeur LOGICAL, vous êtes déjà hors spécifications du côté Fortran! Vous dites:

Un fortran logical*1 A la même représentation que bool, mais dans fortran si les bits sont 00000011 c'est true, en C++ il n'est pas défini.

Cette dernière affirmation n'est pas vraie, la norme Fortran est indépendante de la représentation et l'ABI dit explicitement le contraire. En effet, vous pouvez voir cela en action facilement en en vérifiant la sortie de gfort pour LOGICAL comparaison :

integer function logical_compare(x, y)
    logical, intent(in) :: x
    logical, intent(in) :: y
    if (x .eqv. y) then
        logical_compare = 12
    else
        logical_compare = 24
    end if
end function logical_compare

devient

logical_compare_:
        mov     eax, DWORD PTR [rsi]
        mov     edx, 24
        cmp     DWORD PTR [rdi], eax
        mov     eax, 12
        cmovne  eax, edx
        ret

Vous remarquerez qu'il y a un cmp droit entre les deux valeurs, sans les normaliser d'abord (contrairement à ifort, qui est plus conservateur à cet égard).

Encore plus intéressant: indépendamment de ce que dit l'ABI, ifort utilise par défaut une représentation non standard pour LOGICAL; ceci est expliqué dans la documentation du commutateur -fpscomp logicals , qui spécifie également des détails intéressants sur LOGICAL et la compatibilité entre les langues:

Spécifie que les entiers avec une valeur non nulle sont traités comme vrais, les entiers avec une valeur zéro sont traités comme faux. La constante littérale .TRUE. a une valeur entière de 1 et la constante littérale .FALSE. a une valeur entière de 0. Cette représentation est utilisée par les versions d'Intel Fortran avant la version 8.0 et par Fortran PowerStation.

La valeur par défaut est fpscomp nologicals, Qui spécifie que les valeurs entières impaires (bit faible un) sont traitées comme vraies et les valeurs entières paires (bit faible zéro) sont traitées comme fausses.

La constante littérale .TRUE. a une valeur entière de -1 et la constante littérale .FALSE. a une valeur entière de 0. Cette représentation est utilisée par Compaq Visual Fortran. La représentation interne des valeurs LOGIQUES n'est pas spécifiée par la norme Fortran. Les programmes qui utilisent des valeurs entières dans des contextes LOGICAL, ou qui transmettent des valeurs LOGICAL à des procédures écrites dans d'autres langages, ne sont pas portables et peuvent ne pas s'exécuter correctement. Intel vous recommande d'éviter les pratiques de codage qui dépendent de la représentation interne des valeurs LOGIQUES.

(pas d'italique dans l'original)

Maintenant, la représentation interne d'un LOGICAL ne devrait normalement pas poser de problème, car, d'après ce que je comprends, si vous jouez "selon les règles" et ne franchissez pas les frontières linguistiques, vous n'allez pas le remarquer. Pour un programme conforme standard, il n'y a pas de "conversion directe" entre INTEGER et LOGICAL; la seule façon dont je vois que vous pouvez pousser un INTEGER dans un LOGICAL semble être TRANSFER, qui est intrinsèquement non portable et ne donne aucune garantie réelle, ou le non standard INTEGER <-> LOGICAL conversion lors de l'affectation.

Ce dernier est documenté par gfort pour toujours avoir pour résultat différent de zéro -> .TRUE., Zéro -> .FALSE., Et vous pouvez voir que dans tous les cas, du code est généré pour que cela se produise (même s'il s'agit d'un code alambiqué en cas d'ifort avec la représentation héritée), vous ne pouvez donc pas sembler pousser un entier arbitraire dans un LOGICAL de cette manière.

logical*1 function integer_to_logical(x)
    integer, intent(in) :: x
    integer_to_logical = x
    return
end function integer_to_logical
integer_to_logical_:
        mov     eax, DWORD PTR [rdi]
        test    eax, eax
        setne   al
        ret

La conversion inverse pour un LOGICAL*1 Est une extension zéro entière simple (gfort), donc, pour respecter le contrat dans la documentation liée ci-dessus, il est clair que la valeur LOGICAL sera 0 ou 1.

Mais en général, la situation pour ces conversions est n pe de n gâchis , donc je resterais loin d'eux.


Donc, pour faire court: évitez de mettre des données INTEGER dans des valeurs LOGICAL, car elles sont mauvaises même dans Fortran, et assurez-vous d'utiliser le bon indicateur de compilation pour obtenir la représentation conforme ABI pour les booléens et l'interopérabilité avec C/C++ devrait être correcte. Mais pour plus de sécurité, j'utiliserais simplement char du côté C++.

Enfin, d'après ce que je comprends de la documentation , dans ifort, il existe un support intégré pour l'interopérabilité avec C, y compris les booléens; vous pouvez essayer d'en tirer parti.

65
Matteo Italia

C'est ce qui se produit lorsque vous violez votre contrat avec la langue et le compilateur.

Vous avez probablement entendu quelque part que "zéro est faux" et "non nul est vrai". Cela vaut lorsque vous vous en tenez aux paramètres du langage, convertissant statiquement un int en bool ou vice versa.

Il ne tient pas lorsque vous commencez à jouer avec les représentations binaires. Dans ce cas, vous rompez votre contrat et entrez dans le domaine (au moins) du comportement défini par l'implémentation.

Ne faites pas ça.

Ce n'est pas à vous de savoir comment un bool est stocké en mémoire. C'est au compilateur. Si vous souhaitez modifier la valeur d'un bool, affectez true/false, ou affectez un entier et utilisez les mécanismes de conversion appropriés fournis par C++.


Le standard C++ utilisé pour donner un rappel spécifique sur la façon dont l'utilisation de bool de cette manière est vilain, mauvais et mauvais ( "Utilisation d'une valeur bool d'une manière décrite par ce document comme 'non définie', comme en examinant la valeur d'un objet automatique non initialisé, pourrait le faire se comporter comme s'il n'était ni true ni false. "), même si c'était supprimé en C++ 20 pour des raisons éditoriales .