Je sais qu'un "comportement non défini" en C ++ peut à peu près permettre au compilateur de faire tout ce qu'il veut. Cependant, j'ai eu un crash qui m'a surpris, car j'ai supposé que le code était suffisamment sûr.
Dans ce cas, le vrai problème ne s'est produit que sur une plate-forme spécifique à l'aide d'un compilateur spécifique, et uniquement si l'optimisation a été activée.
J'ai essayé plusieurs choses afin de reproduire le problème et de le simplifier au maximum. Voici un extrait d'une fonction appelée Serialize
, qui prendrait un paramètre booléen et copierait la chaîne true
ou false
dans un tampon de destination existant.
Cette fonction serait-elle dans une révision de code, il n'y aurait aucun moyen de dire qu'elle pourrait en fait planter si le paramètre bool était une valeur non initialisée?
// Zero-filled global buffer of 16 characters
char destBuffer[16];
void Serialize(bool boolValue) {
// Determine which string to print based on boolValue
const char* whichString = boolValue ? "true" : "false";
// Compute the length of the string we selected
const size_t len = strlen(whichString);
// Copy string into destination buffer, which is zero-filled (thus already null-terminated)
memcpy(destBuffer, whichString, len);
}
Si ce code est exécuté avec les optimisations de clang 5.0.0 +, il peut / peut se bloquer.
L'opérateur ternaire attendu boolValue ? "true" : "false"
paraissait assez sûr pour moi, je supposais, "Quelle que soit la valeur de la poubelle, cela boolValue
n'a pas d'importance, car elle sera évaluée comme vraie ou fausse de toute façon."
J'ai configuré un exemple d'Explorateur de compilateur qui montre le problème dans le démontage, voici l'exemple complet. Remarque: afin de reprocher le problème, la combinaison que j'ai trouvée fonctionne en utilisant Clang 5.0.0 avec l'optimisation -O2.
#include <iostream>
#include <cstring>
// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
bool uninitializedBool;
__attribute__ ((noinline)) // Note: the constructor must be declared noinline to trigger the problem
FStruct() {};
};
char destBuffer[16];
// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
// Determine which string to print depending if 'boolValue' is evaluated as true or false
const char* whichString = boolValue ? "true" : "false";
// Compute the length of the string we selected
size_t len = strlen(whichString);
memcpy(destBuffer, whichString, len);
}
int main()
{
// Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
FStruct structInstance;
// Output "true" or "false" to stdout
Serialize(structInstance.uninitializedBool);
return 0;
}
Le problème se pose à cause de l'optimiseur: il était assez intelligent pour déduire que les chaînes "true" et "false" ne diffèrent que par la longueur de 1. Donc, au lieu de vraiment calculer la longueur, il utilise la valeur du bool lui-même, qui devrait techniquement soit 0 ou 1, et va comme ceci:
const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue; // clang clever optimization
Bien que cela soit "intelligent", pour ainsi dire, ma question est la suivante: le standard C ++ permet-il à un compilateur de supposer qu'un booléen ne peut avoir qu'une représentation numérique interne de "0" ou "1" et de l'utiliser de cette manière?
Ou s'agit-il d'un cas défini par l'implémentation, auquel cas l'implémentation a supposé que tous ses bools ne contiendront que 0 ou 1, et toute autre valeur est un territoire de comportement indéfini?
true
» est une règle concernant les opérations booléennes, y compris «l'affectation à un booléen» (qui pourrait implicitement invoquer un enstatic_cast<bool>()
fonction des spécificités). Il ne s'agit cependant pas d'une exigence sur la représentation interne d'unbool
choisi par le compilateur.Réponses:
Oui, ISO C ++ permet (mais ne nécessite pas) des implémentations pour faire ce choix.
Mais notez également que ISO C ++ permet à un compilateur d'émettre du code qui se bloque exprès (par exemple avec une instruction illégale) si le programme rencontre UB, par exemple comme un moyen de vous aider à trouver des erreurs. (Ou parce que c'est une DeathStation 9000. Être strictement conforme n'est pas suffisant pour qu'une implémentation C ++ soit utile dans un but réel). Ainsi, ISO C ++ permettrait à un compilateur de faire un asm qui s'est écrasé (pour des raisons totalement différentes) même sur un code similaire qui lit un fichier non initialisé
uint32_t
. Même si cela doit être un type à disposition fixe sans représentation d'interruption.C'est une question intéressante sur le fonctionnement des implémentations réelles, mais rappelez-vous que même si la réponse était différente, votre code serait toujours dangereux car le C ++ moderne n'est pas une version portable du langage d'assemblage.
Vous compilez pour le système V86 x86-64 ABI , qui spécifie qu'un en
bool
tant que fonction arg dans un registre est représenté par les modèles de bitsfalse=0
ettrue=1
dans les 8 bits de poids faible du registre 1 . En mémoire,bool
est un type à 1 octet qui doit à nouveau avoir une valeur entière de 0 ou 1.(Un ABI est un ensemble de choix d'implémentation sur lesquels les compilateurs de la même plate-forme s'accordent pour qu'ils puissent créer du code qui appelle les fonctions les uns des autres, y compris les tailles de type, les règles de disposition de structure et les conventions d'appel.)
ISO C ++ ne le spécifie pas, mais cette décision ABI est répandue car elle rend la conversion bool-> int bon marché (juste une extension zéro) . Je ne connais aucun ABI qui ne laisse pas le compilateur assumer 0 ou 1 pour
bool
, pour n'importe quelle architecture (pas seulement x86). Il permet des optimisations comme!mybool
avecxor eax,1
pour inverser le bit bas: Tout code possible qui peut inverser un bit / entier / booléen entre 0 et 1 en instruction CPU unique . Ou la compilationa&&b
sur un ET au niveau du bit pour lesbool
types. Certains compilateurs profitent en fait des valeurs booléennes de 8 bits dans les compilateurs. Les opérations sur eux sont-elles inefficaces? .En général, la règle as-if permet au compilateur de tirer parti des informations qui sont vraies sur la plate-forme cible à compiler , car le résultat final sera un code exécutable qui implémentera le même comportement visible de l'extérieur que la source C ++. (Avec toutes les restrictions que le comportement indéfini place sur ce qui est réellement "visible de l'extérieur": non pas avec un débogueur, mais à partir d'un autre thread dans un programme C ++ bien formé / légal.)
Le compilateur est certainement autorisé à profiter pleinement d'une garantie ABI dans son code-gen, et rendre le code comme vous avez trouvé ce qui permet d' optimiser
strlen(whichString)
à5U - boolValue
. (BTW, cette optimisation est assez intelligente, mais peut-être à courte vue par rapport à la ramification et à l'inline enmemcpy
tant que magasins de données immédiates 2. )Ou le compilateur aurait pu créer une table de pointeurs et l'indexer avec la valeur entière de la
bool
, en supposant à nouveau qu'il s'agissait d'un 0 ou 1. ( Cette possibilité est ce que la réponse de @ Barmar a suggéré .)Votre
__attribute((noinline))
constructeur avec l'optimisation activée a conduit à claquer juste le chargement d'un octet de la pile pour l'utiliser commeuninitializedBool
. Il a fait l' espace pour l'objetmain
avecpush rax
( ce qui est plus petit et pour diverses raisons à peu près aussi efficace quesub rsp, 8
), de sorte que tout ce qui était dans les ordures AL à l' entrée demain
la valeur qu'elle utilisée pouruninitializedBool
. C'est pourquoi vous avez en fait obtenu des valeurs qui n'étaient pas seulement0
.5U - random garbage
peut facilement encapsuler une grande valeur non signée, ce qui amène memcpy à entrer dans la mémoire non mappée. La destination est en stockage statique, pas la pile, donc vous n'écrasez pas une adresse de retour ou quelque chose.D'autres implémentations pourraient faire des choix différents, par exemple
false=0
ettrue=any non-zero value
. Ensuite, clang ne produirait probablement pas de code qui se bloque pour cette instance spécifique d'UB. (Mais il serait toujours autorisé à le faire s'il le voulait.) Je ne connais aucune implémentation qui choisisse autre chose que ce que fait x86-64bool
, mais la norme C ++ autorise beaucoup de choses que personne ne fait ou ne voudrait faire sur matériel qui ressemble à des processeurs actuels.ISO C ++ ne précise pas ce que vous trouverez lorsque vous examinerez ou modifierez la représentation objet d'un
bool
. (par exemple parmemcpy
ing l'bool
enunsigned char
, que vous êtes autorisé à le faire parce quechar*
tout peut alias. Etunsigned char
est garanti d'avoir aucun bit de remplissage, de sorte que le standard C ++ ne vous permet formellement HexDump représentations d'objets sans UB. Pointer-casting pour copier l'objet la représentation est différente de l'affectationchar foo = my_bool
, bien sûr, donc la booléenisation à 0 ou 1 ne se produirait pas et vous obtiendriez la représentation d'objet brut.)Vous avez partiellement "caché" l'UB sur ce chemin d'exécution du compilateur avec
noinline
. Même si elle n'est pas en ligne, cependant, les optimisations interprocédurales pourraient toujours créer une version de la fonction qui dépend de la définition d'une autre fonction. (Premièrement, clang crée un exécutable, pas une bibliothèque partagée Unix où l'interposition de symboles peut se produire. Deuxièmement, la définition se trouve à l'intérieur de laclass{}
définition de sorte que toutes les unités de traduction doivent avoir la même définition. Comme avec leinline
mot - clé.)Ainsi, un compilateur pourrait émettre juste un
ret
ouud2
(instruction illégale) comme définition pourmain
, car le chemin d'exécution commençant au sommet demain
rencontre inévitablement un comportement indéfini. (Ce que le compilateur peut voir au moment de la compilation s'il décide de suivre le chemin à travers le constructeur non en ligne.)Tout programme qui rencontre UB est totalement indéfini pour toute son existence. Mais UB à l'intérieur d'une fonction ou d'une
if()
branche qui ne s'exécute jamais ne corrompe pas le reste du programme. En pratique, cela signifie que les compilateurs peuvent décider d'émettre une instruction illégaleret
, ou de ne pas émettre quoi que ce soit et de tomber dans le bloc / fonction suivant, pour l'ensemble du bloc de base qui peut être prouvé au moment de la compilation pour contenir ou conduire à UB.GCC et Clang dans la pratique ne fait parfois émettent
ud2
sur UB, au lieu de même essayer de générer du code pour les chemins d'exécution qui ne font pas de sens. Ou pour des cas comme tomber de la fin d'une non-void
fonction, gcc omettra parfois uneret
instruction. Si vous pensiez que "ma fonction ne fera que revenir avec les ordures dans RAX", vous vous trompez profondément. Les compilateurs C ++ modernes ne traitent plus le langage comme un langage d'assemblage portable. Votre programme doit vraiment être C ++ valide, sans faire d'hypothèses sur l'apparence d'une version autonome non intégrée de votre fonction dans asm.Un autre exemple amusant est: Pourquoi l'accès non aligné à la mémoire mmap est-il parfois un défaut de segmentation sur AMD64? . x86 ne fait pas défaut sur les entiers non alignés, non? Alors pourquoi un mauvais alignement
uint16_t*
serait-il un problème? Parce quealignof(uint16_t) == 2
, et violer cette hypothèse a conduit à une erreur de segmentation lors de la vectorisation automatique avec SSE2.Voir aussi Ce que tout programmeur C devrait savoir sur le comportement indéfini # 1/3, un article d'un développeur clang.
Point clé: si le compilateur a remarqué l'UB au moment de la compilation, il pourrait "casser" (émettre un asm surprenant) le chemin à travers votre code qui provoque UB même s'il cible un ABI où n'importe quel motif binaire est une représentation d'objet valide pour
bool
.Attendez-vous à une hostilité totale envers de nombreuses erreurs de la part du programmeur, en particulier les choses que les compilateurs modernes mettent en garde. C'est pourquoi vous devez utiliser
-Wall
et corriger les avertissements. C ++ n'est pas un langage convivial, et quelque chose en C ++ peut être dangereux même s'il serait sûr en asm sur la cible pour laquelle vous compilez. (Par exemple, le débordement signé est UB en C ++ et les compilateurs supposeront que cela ne se produit pas, même lors de la compilation pour le complément x86 à 2, sauf si vous l'utilisezclang/gcc -fwrapv
.)L'UB visible à la compilation est toujours dangereux, et il est vraiment difficile d'être sûr (avec l'optimisation de la liaison) que vous avez vraiment caché l'UB au compilateur et pouvez donc raisonner sur le type d'asm qu'il générera.
Ne pas être trop dramatique; Souvent, les compilateurs vous permettent de vous en sortir avec certaines choses et d'émettre du code comme vous vous attendez même lorsque quelque chose est UB. Mais ce sera peut-être un problème à l'avenir si les développeurs du compilateur implémentent une optimisation qui obtient plus d'informations sur les plages de valeurs (par exemple, qu'une variable n'est pas négative, lui permettant peut-être d'optimiser l'extension de signe pour libérer l'extension zéro sur x86- 64). Par exemple, dans gcc et clang actuels, faire
tmp = a+INT_MIN
ne s'optimise pasa<0
comme toujours faux, mais c'esttmp
toujours négatif. (Parce queINT_MIN
+a=INT_MAX
est négatif sur la cible de complément de 2 eta
ne peut pas être supérieur à cela.)Donc, gcc / clang ne fait pas actuellement marche arrière pour dériver les informations de plage pour les entrées d'un calcul, uniquement sur les résultats basés sur l'hypothèse d'aucun débordement signé: exemple sur Godbolt . Je ne sais pas si cette optimisation est intentionnellement «manquée» au nom de la convivialité ou quoi.
Notez également que les implémentations (alias compilateurs) sont autorisées à définir le comportement qu'ISO C ++ laisse non défini . Par exemple, tous les compilateurs qui prennent en charge les intrinsèques d'Intel (comme
_mm_add_ps(__m128, __m128)
pour la vectorisation SIMD manuelle) doivent permettre de former des pointeurs mal alignés, ce qui est UB en C ++ même si vous ne les déréférencez pas.__m128i _mm_loadu_si128(const __m128i *)
effectue des charges non alignées en prenant un__m128i*
argument mal aligné , pas unvoid*
ouchar*
. Est-ce que `reinterpret_cast`ing entre le pointeur vectoriel matériel et le type correspondant est un comportement non défini?GNU C / C ++ définit également le comportement de décalage à gauche d'un nombre signé négatif (même sans
-fwrapv
), séparément des règles UB normales de débordement signé. ( Il s'agit d'UB dans ISO C ++ , tandis que les décalages à droite des nombres signés sont définis par l'implémentation (logique ou arithmétique); des implémentations de bonne qualité choisissent l'arithmétique sur HW qui a des décalages à droite arithmétiques, mais ISO C ++ ne spécifie pas). Ceci est documenté dans la section Integer du manuel GCC , ainsi que la définition du comportement défini par l'implémentation que les normes C nécessitent que les implémentations définissent d'une manière ou d'une autre.Il y a certainement des problèmes de qualité de mise en œuvre qui intéressent les développeurs de compilateurs; ils n'essaient généralement pas de faire des compilateurs intentionnellement hostiles, mais tirer parti de tous les nids-de-poule UB en C ++ (sauf ceux qu'ils choisissent de définir) pour mieux optimiser peut parfois être presque impossible à distinguer.
Note de bas de page 1 : Les 56 bits supérieurs peuvent être des ordures que l'appelé doit ignorer, comme d'habitude pour les types plus étroits qu'un registre.
( D' autres ABIs font faire des choix différents ici . Certains ne nécessitent des types entiers étroits pour être ou signe-zéro étendu pour remplir un registre lorsqu'il est passé ou retour de fonctions, comme MIPS64 et PowerPC64. Voir la dernière section de cette réponse x86-64 qui compare avec les ISA antérieures .)
Par exemple, un appelant peut avoir calculé
a & 0x01010101
en RDI et l'utiliser pour autre chose, avant d'appelerbool_func(a&1)
. L'appelant pourrait optimiser le&1
car il l'a déjà fait pour l'octet bas dans le cadre deand edi, 0x01010101
, et il sait que l'appelé doit ignorer les octets élevés.Ou si un booléen est passé comme 3e argument, peut-être qu'un appelant optimisant pour la taille du code le charge avec
mov dl, [mem]
au lieu demovzx edx, [mem]
, économisant 1 octet au prix d'une fausse dépendance à l'ancienne valeur de RDX (ou tout autre effet de registre partiel, selon sur modèle CPU). Ou pour le premier argument,mov dil, byte [r10]
au lieu demovzx edi, byte [r10]
, car les deux nécessitent de toute façon un préfixe REX.C'est pourquoi clang émet
movzx eax, dil
à laSerialize
place desub eax, edi
. (Pour les arguments entiers, clang viole cette règle ABI, en fonction du comportement non documenté de gcc et clang à zéro ou à extension de signe des entiers étroits à 32 bits. Un signe ou une extension zéro est-il requis lors de l'ajout d'un décalage de 32 bits à un pointeur pour le x86-64 ABI? J'ai donc été intéressé de voir qu'il ne fait pas la même chose pourbool
.)Note de bas de page 2: après la
mov
création d'un branchement, vous disposez simplement d'un magasin à 4 octets immédiat ou d'un magasin à 4 octets + 1 octet. La longueur est implicite dans les largeurs de magasin + décalages.OTOH, glibc memcpy fera deux chargements / magasins de 4 octets avec un chevauchement qui dépend de la longueur, donc cela finit vraiment par rendre le tout exempt de branches conditionnelles sur le booléen. Voir le
L(between_4_7):
bloc dans memcpy / memmove de glibc. Ou du moins, procédez de la même manière pour chaque booléen dans la branche de memcpy pour sélectionner une taille de bloc.Si vous êtes en ligne, vous pouvez utiliser 2x
mov
-immediate +cmov
et un décalage conditionnel, ou vous pouvez laisser les données de chaîne en mémoire.Ou si le réglage pour Intel Ice Lake ( avec la fonction Fast Short REP MOV ), un réel
rep movsb
peut être optimal. glibcmemcpy
peut commencer à utiliserrep movsb
pour les petites tailles sur les processeurs avec cette fonctionnalité, économisant ainsi beaucoup de branchements.Outils de détection d'UB et d'utilisation de valeurs non initialisées
Dans gcc et clang, vous pouvez compiler avec
-fsanitize=undefined
pour ajouter une instrumentation d'exécution qui avertira ou générera une erreur sur UB qui se produit lors de l'exécution. Cependant, cela n'acceptera pas les variables unitarisées. (Parce qu'il n'augmente pas la taille des caractères pour faire de la place pour un bit "non initialisé").Voir https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/
Pour trouver l'utilisation des données non initialisées, il existe un assainisseur d'adresse et un assainisseur de mémoire dans clang / LLVM. https://github.com/google/sanitizers/wiki/MemorySanitizer montre des exemples de
clang -fsanitize=memory -fPIE -pie
détection de lectures de mémoire non initialisées. Cela peut fonctionner mieux si vous compilez sans optimisation, donc toutes les lectures de variables finissent par se charger réellement à partir de la mémoire dans l'asm. Ils montrent qu'il est utilisé-O2
dans un cas où la charge ne serait pas optimisée. Je ne l'ai pas essayé moi-même. (Dans certains cas, par exemple en n'initialisant pas un accumulateur avant de sommer un tableau, clang -O3 émettra du code qui résume dans un registre vectoriel qu'il n'a jamais initialisé. Ainsi, avec l'optimisation, vous pouvez avoir un cas où il n'y a pas de lecture de mémoire associée à l'UB . Mais-fsanitize=memory
modifie l'asm généré et peut entraîner une vérification.)Cela devrait fonctionner dans ce cas, car l'appel à glibc
memcpy
avec unelength
mémoire calculée à partir de la mémoire non initialisée entraînera (à l'intérieur de la bibliothèque) une branche basée surlength
. S'il avait intégré une version entièrement sans branche qui vient d'utilisercmov
, l'indexation et deux magasins, cela n'aurait peut-être pas fonctionné.Valgrind
memcheck
recherchera également ce type de problème, ne se plaignant pas non plus si le programme copie simplement des données non initialisées. Mais il dit qu'il détectera quand un "saut ou déplacement conditionnel dépend de valeurs non initialisées", pour essayer d'attraper tout comportement visible de l'extérieur qui dépend de données non initialisées.Peut-être que l'idée de ne pas signaler uniquement une charge est que les structures peuvent avoir un remplissage, et la copie de la structure entière (y compris le remplissage) avec un large chargement / stockage vectoriel n'est pas une erreur même si les membres individuels n'ont été écrits qu'un par un. Au niveau asm, les informations sur ce qui était du remplissage et ce qui fait réellement partie de la valeur ont été perdues.
la source
Le compilateur est autorisé à supposer qu'une valeur booléenne passée en argument est une valeur booléenne valide (c'est-à-dire qui a été initialisée ou convertie en
true
oufalse
). Latrue
valeur ne doit pas nécessairement être la même que l'entier 1 - en effet, il peut y avoir diverses représentations detrue
etfalse
- mais le paramètre doit être une représentation valide de l'une de ces deux valeurs, où "représentation valide" est l'implémentation - défini.Donc, si vous ne parvenez pas à initialiser un
bool
, ou si vous réussissez à l'écraser via un pointeur d'un type différent, les hypothèses du compilateur seront erronées et un comportement indéfini s'ensuivra. Vous aviez été prévenu:la source
true
valeur ne doit pas nécessairement être la même que l'entier 1" est en quelque sorte trompeuse. Bien sûr, le modèle binaire réel pourrait être autre chose, mais lorsqu'il est implicitement converti / promu (la seule façon dont vous verriez une valeur autre quetrue
/false
),true
est toujours1
etfalse
est toujours0
. Bien sûr, un tel compilateur serait également incapable d'utiliser l'astuce que ce compilateur essayait d'utiliser (en utilisant le fait quebool
le modèle de bits réel ne pouvait être que0
ou1
), donc c'est un peu sans rapport avec le problème de l'OP.true
au motif binaire1
, c'est sa prérogative. S'il choisit un autre ensemble de représentations, il ne pourrait en effet pas utiliser l'optimisation notée ici. S'il choisit cette représentation particulière, il le peut. Il doit seulement être cohérent en interne. Vous pouvez examiner la représentation d'unbool
en le copiant dans un tableau d'octets; ce n'est pas UB (mais il est défini par l'implémentation)bool
présence d'un modèle binaire de0
ou1
. Ils ne re-booléensent pasbool
chaque fois qu'ils le lisent depuis la mémoire (ou un registre contenant une fonction arg). Voilà ce que dit cette réponse. exemples : gcc4.7 + peut optimiserreturn a||b
versor eax, edi
dans une fonction de retourbool
, ou MSVC peut optimisera&b
verstest cl, dl
. x86test
est un bitand
, donc sicl=1
etdl=2
test définit les drapeaux seloncl&dl = 0
.La fonction elle-même est correcte, mais dans votre programme de test, l'instruction qui appelle la fonction provoque un comportement non défini en utilisant la valeur d'une variable non initialisée.
Le bogue se trouve dans la fonction appelante et il pourrait être détecté par un examen du code ou une analyse statique de la fonction appelante. En utilisant votre lien d'explorateur de compilateur, le compilateur gcc 8.2 détecte le bogue. (Vous pourriez peut-être déposer un rapport de bogue contre clang qu'il ne trouve pas le problème).
Un comportement indéfini signifie que tout peut arriver, ce qui inclut le programme qui plante quelques lignes après l'événement qui a déclenché le comportement indéfini.
NB. La réponse à "Un comportement indéfini peut-il provoquer _____?" est toujours "Oui". C'est littéralement la définition d'un comportement indéfini.
la source
bool
déclencheur UB non initialisé ?bool
). La copie nécessite l'évaluation de la sourcetrue
,false
et desnot-a-thing
valeurs pour booléens.Un booléen est uniquement autorisé à contenir les valeurs dépendant de l'implémentation utilisées en interne pour
true
etfalse
, et le code généré peut supposer qu'il ne contiendra qu'une seule de ces deux valeurs.En règle générale, l'implémentation utilisera l'entier
0
pourfalse
et1
pourtrue
, pour simplifier les conversions entrebool
etint
, et pourif (boolvar)
générer le même code queif (intvar)
. Dans ce cas, on peut imaginer que le code généré pour le ternaire dans l'affectation utiliserait la valeur comme index dans un tableau de pointeurs vers les deux chaînes, c'est-à-dire qu'il pourrait être converti en quelque chose comme:S'il
boolValue
n'est pas initialisé, il pourrait en fait contenir n'importe quelle valeur entière, ce qui entraînerait alors un accès en dehors des limites dustrings
tableau.la source
bool
àint
avec*(int *)&boolValue
et imprimez-le à des fins de débogage, voyez s'il s'agit de quelque chose d'autre0
ou1
quand il se bloque. Si tel est le cas, cela confirme à peu près la théorie selon laquelle le compilateur optimise l'inline-if sous forme de tableau, ce qui explique pourquoi il se bloque.std::bitset<8>
ne me donne pas de bons noms pour tous mes différents drapeaux. Selon ce qu'ils sont, cela peut être important.En résumant beaucoup votre question, vous vous demandez si la norme C ++ permet à un compilateur de supposer qu'un
bool
ne peut avoir qu'une représentation numérique interne de «0» ou «1» et de l'utiliser de cette manière?La norme ne dit rien sur la représentation interne d'un
bool
. Il définit uniquement ce qui se passe lors de la conversion d'unbool
vers unint
(ou vice versa). Généralement, en raison de ces conversions intégrales (et du fait que les gens y dépendent plutôt), le compilateur utilisera 0 et 1, mais il n'est pas obligé (bien qu'il doive respecter les contraintes de tout ABI de niveau inférieur qu'il utilise ).Ainsi, le compilateur, lorsqu'il voit un,
bool
est en droit de considérer que leditbool
contient l'un des modèles de bits 'true
' ou 'false
' et de faire tout ce qu'il ressent. Donc , si les valeurstrue
etfalse
sont 1 et 0, respectivement, le compilateur est en effet permis d'optimiserstrlen
à5 - <boolean value>
. D'autres comportements amusants sont possibles!Comme indiqué à plusieurs reprises ici, un comportement indéfini a des résultats indéfinis. Y compris, mais sans s'y limiter
Voir ce que chaque programmeur doit savoir sur le comportement non défini
la source