I. Introduction▲
Salut les Terriens. Bienvenue dans ce septième opus de la série d'articles sur le « C/C++ bas niveau » que j'écris. Cet article parle de l'opérateur conditionnel et de l'instruction switch. Comme d'habitude, je m'appuierai sur des morceaux de code C++ et leurs correspondances en assembleur x86 (celui produit par VS2010) pour vous montrer ce que votre code en langage haut niveau devient au niveau de l'assembleur.
Avertissement : dans un monde idéal, je voudrais essayer d'éviter trop de connaissances supposées, mais en regardant le niveau de détail de chaque article, c'est franchement, trop de travail. Par conséquent, je vais, dès à présent, vous renvoyer vers l'article 6 avant de continuer avec cet article…
Ensuite, voici les liens pour tous ceux qui veulent commencer dès le début de cette série d'articles (attention : ça peut prendre un certain temps, les premiers sont assez longs) :
- Programme d'étude sur le C++ bas niveau ;
- Programme d'étude sur le C++ bas niveau n° 2 : les types de données ;
- Programme d'étude sur le C++ bas niveau n° 3 : la Pile ;
- Programme d'étude sur le C++ bas niveau n° 4 : plus de Pile ;
- Programme d'étude sur le C++ bas niveau n° 5 : encore plus de Pile ;
- Programme d'étude sur le C++ bas niveau n° 6 : les conditions (voyez au début de cet article les détails pour compiler et exécuter le code de démonstration).
II. L'opérateur conditionnel▲
Je suppose que vous êtes familier avec l'opérateur conditionnel aussi appelé « point d'interrogation » ou encore « opérateur ternaire » (ternaire parce que c'est le seul opérateur en C/C++ qui prend trois opérandes).
Si vous ne l'êtes pas, voici un lien qui vous permettra de comprendre (je parie que vous serez tellement content d'en savoir plus à ce sujet que vous saurez tout dans la semaine).
Personnellement, j'approuve entièrement l'usage de l'opérateur conditionnel lorsqu'il est utilisé judicieusement, mais il n'est pas toujours idéal pour le débogage au niveau source, car il s'agit essentiellement d'une seule ligne if
-
else
et cela peut être difficile à suivre dans le débogueur (en fait, j'ai entendu dire qu'il était banni par certaines normes de codage de plusieurs entreprises, mais c'est ainsi, nous ne pouvons pas tous être sain d'esprit, n'est-ce pas ?)
Quoi qu'il en soit, nous allons y jeter un coup d'œil sur cet opérateur avec un peu de code :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
#include
"stdafx.h"
int
main(int
argc, char
*
argv[])
{
// la ligne après ce commentaire est logiquement équivalente à ce code :
// int iLocal; if( argc > 2 ){ iLocal = 3; }else{ iLocal = 7; }
int
iLocal =
(argc >
2
) ? 3
: 7
;
return
0
;
}
Si vous vous rappelez l'assembleur généré par une instruction basique if
-
else
de l'article précédent, l'assembleur généré ici va probablement vous casser les pieds…
Note :
- j'ai volontairement ignoré l'assembleur du prologue et de l'épilogue dans la fonction ci-dessous et juste montré l'assembleur impliqué dans l'affectation avec l'opérateur conditionnel ;
- si la vue du désassembleur ne vous montre pas les noms de variables, alors vous devez faire un clic droit de la fenêtre et cocher l'option « Show Symbol Names ».
2.
3.
4.
5.
6.
5
: int
iLocal =
(argc >
2
) ? 3
: 7
;
01311249
xor
eax
,eax
0131124B
cmp
dword
ptr
[argc],2
0131124F
setle
al
01311252
lea
eax
,[eax
*
4
+
3
]
01311259
mov
dword
ptr
[iLocal],eax
Il est clair que c'est totalement différent du code if
-
else
que nous avons examiné précédemment.
C'est parce qu'il y a des magouilles et que le compilateur a décidé de faire le sournois avec les branches de code pour mettre en œuvre la logique spécifiée par le code C++.
Nous allons donc l'examiner ligne par ligne :
- la ligne 1 utilise l'instruction
xor
pour initialiser la valeur du registreeax
avec 0. Le « ou exclusif » de n'importe quoi avec lui-même donne 0 ; - la ligne 2, comme dans l'exemple précédent avec
if
, utilise l'instructioncmp
pour tester la condition en positionnant un drapeau dans un registre spécial de la CPU sur la base du résultat de la comparaison ; - à la ligne 3, nous voyons une nouvelle instruction ! L'instruction
setle
(set less equal) initialise son opérande à 1 si le premier opérande ducmp
précédent était inférieur ou égal au deuxième opérande et à 0 s'il était plus grand. Nous n'avons pas vu auparavant l'opérandeal
, il s'agit du nom d'un registre du 386 qui est en fait l'octet de poids faible du registreeax
(si vous êtes attentif et que vous déboguez ce code en pas à pas avec une fenêtre des registres ouverte, vous verrez que cette instruction fait que le registreeax
est mis à 1 - notons aussi que cela ne fonctionne que parce queeax
a déjà été mis à 0) ; - la ligne 4 utilise l'instruction de chargement d'adresse
lea
(load effective address) pour faire des calculs sournois qui reposent sur la valeur du registreeax
fixée par l'instructionsetle
de la ligne 3 ; - la ligne 5 recopie la valeur de
eax
dans l'adresse de mémoire stockant la valeur de la variable iLocal.
C'est très bien, mais comment ça marche ?
Tout d'abord, notez qu'au niveau de l'assembleur l'instruction de comparaison setle
teste (comme dans les exemples de l'article précédent) la condition inverse à celle spécifiée dans le code C++.
Cela signifie que le registre eax
sera mis à 0 dans la ligne 3. Si argc est supérieur à 2, cela fait que [eax
x 4
+
3
] de la ligne 5 sera évalué comme (0 x 4) + 3, soit 3.
À l'inverse, si argc est inférieur ou égal à 2, le registre eax
sera mis à 1 ce qui signifie que la ligne 5 sera évaluée comme (1 x 4) + 3, soit 7.
Donc, comme vous pouvez le voir, l'assembleur exécute la même branche de code indépendamment de l'état, mais en utilisant le résultat 0 ou 1 de l'instruction conditionnelle dans le calcul pour annuler ou inclure un des termes et ainsi donner ce que j'aime appeler un « si mathématique ». Futé.
D'ailleurs ce genre de « condition sans branches » a été un peu / beaucoup un sujet d'actualité au cours des dernières années, en particulier sur les consoles depuis que les processeurs sont particulièrement sensibles aux erreurs de prédiction de saut.
L'utilisation judicieuse de l'idiome « condition sans branches » est un outil qui peut être utilisé pour lutter contre les problèmes de performances avec les erreurs de prédiction de saut - pour un exemple à ce sujet, voir l'utilisation de l'instruction fsel (floating point select) dans cet article écrit par Tony Albrecht et pour une discussion brève des problèmes de prédiction de branchement (liés aux PC), voir cet article écrit par Igor Ostrovsky (qui travaille pour Microsoft).
III. L'opérateur conditionnel (deuxième partie)▲
Donc, clairement notre super-simple code ci-dessus fait que le compilateur va générer un assembleur intelligent à cause des valeurs constantes dans celui-ci. C'est certes intéressant, mais pas nécessairement représentatif de majorité du code du « monde réel ».
Voyons ce qui se passe si nous utilisons des variables avec l'opérateur conditionnel…
2.
3.
4.
5.
6.
7.
8.
9.
10.
#include
"stdafx.h"
int
main(int
argc, char
*
argv[])
{
int
iOperandTwo =
3
;
int
iOperandThree =
7
;
int
iLocal =
(argc >
2
) ? iOperandTwo : iOperandThree;
return
0
;
}
Voici la partie pertinente du désassembleur :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
5
: int
iOperandTwo =
3
;
00CF1619
mov
dword
ptr
[iOperandTwo],3
6
: int
iOperandThree =
7
;
00CF1620
mov
dword
ptr
[iOperandThree],7
7
: int
iLocal =
(argc >
2
) ? iOperandTwo : iOperandThree;
00CF1627
cmp
dword
ptr
[argc],2
00CF162B
jle
main+
25h
(0CF1635h
)
00CF162D
mov
eax
,dword
ptr
[iOperandTwo]
00CF1630
mov
dword
ptr
[ebp
-
50h
],eax
00CF1633
jmp
main+
2Bh
(0CF163Bh
)
00CF1635
mov
ecx
,dword
ptr
[iOperandThree]
00CF1638
mov
dword
ptr
[ebp
-
50h
],ecx
00CF163B
mov
edx
,dword
ptr
[ebp
-
50h
]
00CF163E
mov
dword
ptr
[iLocal],edx
Maintenant que l'opérateur conditionnel doit utiliser des variables, nous nous attendions à ce qu'il génère quelque chose qui ressemble plus au genre de code que nous avons vu la dernière fois avec une instruction if
-
else
basique.
Nous avons l'instruction cmp
attendue qui teste la condition inverse suivie d'un saut conditionnel, puis deux blocs d'assembleur. Ce premier bloc (lignes 7 à 9) fait un saut inconditionnel au-dessus du second (lignes 10 et 11) s'il est exécuté. Donc effectivement, cela se passe à peu près comme prévu, mais il a clairement des choses intéressantes qui se passent dedans.
- Les deux blocs utilisent des registres différents pour stocker leurs valeurs intermédiaires, le premier utilise
eax
, le second utiliseecx
. - Les deux branches stockent leur résultat à la même adresse mémoire dans la Pile (voir cet article si vous ne savez pas ou plus ce qu'est le « Stack Frame »), c'est-à-dire [
ebp
-
50h
]. - Le code qui attribue la valeur à iLocal (lignes 12 et 13) n'existe qu'une fois et est exécuté quelle que soit la branche qui a été utilisée, elle lit la valeur de [
ebp
-
50h
] et l'écrit dans iLocal en utilisant un troisième registre (edx
).
L'utilisation de registres différents pour les différentes branches de l'étape 1 semble être importante, mais (selon les sources de plusieurs experts), c'est apparemment un comportement parfaitement normal du compilateur et il n'a rien à en redire.
Les étapes 2 et 3 montrent que le code généré par l'opérateur conditionnel (au moins avec VS2010) n'est pas directement équivalent à son équivalent intuitif if
-
else
:
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
// équivalent intuitif if-else de
// int iLocal = (argc > 2 ) ? iOperandTwo : iOperandThree;
int
iLocal;
if
( argc >
2
)
{
iLocal =
iOperandTwo;
}
else
{
iLocal =
iOperandThree;
}
Plutôt que de choisir entre l'une des deux affectations comme dans ce if
-
else
, l'assembleur généré pour cette utilisation de l'opérateur conditionnel fait exactement ce que nous lui avons dit : choisir l'une des deux valeurs (la stocker temporairement dans la Pile) et l'assigner à iLocal.
Quelques remarques finales sur l'opérateur conditionnel :
- vous voyez bien que « moins de lignes de code C++ » n'est pas synonyme de « moins d'assembleur » ;
- il peut être imbriqué, mais ne le faites pas ! C'est affreux et sera également très difficile à suivre lors du débogage au niveau du code ;
- soyez très prudent avec la priorité des opérateurs lors de son utilisation. Utilisez les parenthèses pour vous assurer qu'il va bien résoudre comme vous en avez l'intention.
IV. L'instruction switch▲
Le dernier type de déclaration conditionnelle que nous allons examiner est l'instruction switch
. Comme l'opérateur conditionnel, l'instruction switch
est une construction souvent maltraitée et décriée, mais vous ne voudriez pas vivre sans.
Pour être juste avec l'instruction switch
, ce n'est pas la faute à cause d'elle qu'il est possible pour les maniaques d'écrire du code fragile et pour les fous de l'utiliser.
IV-A. Un aparté au sujet de l'instruction switch▲
Toutes les fois où j'ai toujours trouvé des exemples vraiment horribles de déclarations switch
, c'est quand un système à l'origine synchrone et sans état a été transformé pour devenir asynchrone avec état sous la pression du temps. Cette situation semble toujours produire le genre de déclarations switch
monolithiques, difficile à suivre, difficile à maintenir, d'une architecture fragile qui a donné à l'instruction switch
une mauvaise réputation au fil des ans.
Un code qui a eu des fonctionnalités réseau modernisées (à mon avis) un endroit très fréquent de trouver des instructions switch
à problèmes. Il est toujours préférable de fixer un système correctement s'il semble qu'il soit systématiquement cassé que de vouloir le persévérer. S'il semble que vous deviez mettre en place un ensemble d'états dans un système, il est architecturalement plus judicieux d'utiliser un comportement polymorphique (par exemple, une classe d'états avec une ou plusieurs fonctions virtuelles) plutôt qu'une instruction switch
.
IV-B. Où en étais-je ?▲
Désolé, allons-y et jetons un coup d'œil à une instruction switch
…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
#include
"stdafx.h"
int
main(int
argc, char
*
argv[])
{
int
iLocal =
0
;
// n.b. no "break" in case 1 so we can
// see what "fall through" looks like
switch
( argc )
{
case
1
:
iLocal =
6
;
case
3
:
iLocal =
7
;
break
;
case
5
:
iLocal =
8
;
break
;
default
:
iLocal =
9
;
break
;
}
return
0
;
}
Et voici le désassembleur…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
9
: switch( argc )
00C61620
mov
eax
,dword
ptr
[argc]
00C61623
mov
dword
ptr
[ebp
-
48h
],eax
00C61626
cmp
dword
ptr
[ebp
-
48h
],1
00C6162A
je
main+
2Ah
(0C6163Ah
)
00C6162C
cmp
dword
ptr
[ebp
-
48h
],3
00C61630
je
main+
31h
(0C61641h
)
00C61632
cmp
dword
ptr
[ebp
-
48h
],5
00C61636
je
main+
3Ah
(0C6164Ah
)
00C61638
jmp
main+
43h
(0C61653h
)
10
: {
11
: case 1
:
12
: iLocal =
6
;
00C6163A
mov
dword
ptr
[iLocal],6
13
: case 3
:
14
: iLocal =
7
;
00C61641
mov
dword
ptr
[iLocal],7
15
: break;
00C61648
jmp
main+
4Ah
(0C6165Ah
)
16
: case 5
:
17
: iLocal =
8
;
00C6164A
mov
dword
ptr
[iLocal],8
18
: break;
00C61651
jmp
main+
4Ah
(0C6165Ah
)
19
: default:
20
: iLocal =
9
;
00C61653
mov
dword
ptr
[iLocal],9
21
: break;
22
: }
C'est plus ou moins exactement ce que vous attendiez :
- la ligne 1 recopie argc sur la Pile à [
ebp
-
48h
] ; - ensuite, les lignes 2 à 9 mettent en œuvre la logique du
switch
par une série de comparaisons de cette valeur par rapport aux constantes spécifiées dans les instructionscase
associées aux sauts conditionnels et au code de l'instructioncase
correspondante ; - si aucun des sauts conditionnels n'est déclenché, la logique provoque un saut inconditionnel à l'instruction
default
.
Il faut noter que :
- quand le mot-clé
break
est utilisé, cela provoque un saut inconditionnel à la fin du code de l'instructionswitch
; - le « tomber à travers » (NDT : absence de l'instruction
break
à la fin du bloccase
) du bloccase
1 vers le bloccase
3 dans le code de haut niveau se traduit au niveau de l'assembleur généré par le compilateur par les blocs d'instructions adjacentes de l'instructionswitch
et l'absence de saut inconditionnel à la fin du bloc de code assembleur pour ducase
1.
Si vous regardez l'assembleur de l'exemple if
-
else
-
if
-
else
de l'article précédent, vous devriez être capable de voir que l'assembleur généré pour cette instruction switch
est (plus ou moins) ce qui arriverait si nous l'avions comme if
-
else
-
if
-
else
puis réorganisé l'assembleur. Toute la logique était déjà en place au début et l'assembleur généré pour chaque bloc de code a été laissé là où il était.
Donc, à part le fait que l'instruction switch
soit une fonctionnalité du langage C/C++ très utile pour gérer ce qui serait souvent un sale désordre avec des risques d'erreurs dans les déclarations if
-
else
-
if
-
else
, sur la base de cet exemple, il ne semble pas faire quoi que ce soit qui pourrait offrir un avantage significatif au niveau assembleur. Alors pourquoi aurais-je prétendu que le compilateur peut générer un « assembleur plutôt cool » pour une instruction switch
?
Avant de dire que nous avons tout vu, essayons d'utiliser une plage contiguë de valeurs pour les constantes dans les case
du switch
. Vous savez, juste pour le plaisir, et pour des raisons de simplicité nous allons commencer à 0.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
#include
"stdafx.h"
int
main(int
argc, char
*
argv[])
{
int
iLocal =
0
;
switch
( argc )
{
case
0
:
iLocal =
4
;
break
;
case
1
:
iLocal =
5
;
break
;
case
2
:
iLocal =
6
;
break
;
case
3
:
iLocal =
7
;
break
;
}
return
0
;
}
Et voici le code assembleur :
OK, donc cette fois, quelque chose de plus intéressant est en train de se passer - N.B. : J'ai utilisé une capture d'écran plutôt que de simplement coller le texte, car nous avons besoin de regarder dans une fenêtre de mémoire pour lui donner un sens.
Alors, qu'est-ce que cela fait ?
- Il copie argc dans
eax
et ensuite il le recopie sur la Pile à [ebp
-
48h
], - Il compare ensuite la valeur stockée dans l'adresse [
ebp
-
48h
] avec 3 (la constante la plus grande de nos blocscase
). - Si cette valeur est supérieure à 3 alors l'instruction
ja
de la ligne suivante va provoquer un saut à 8D1658h qui est la première instruction après les blocs de code case et donc sauter l'instructionswitch
. - Si la valeur est plus petite ou égale à 3 alors cette valeur est recopiée dans
ecx
et il y a ensuite un saut inconditionnel vers … quelque part. :-/
OK, ce saut inconditionnel final a une syntaxe que nous n'avons pas encore vue de par son opérande d'adresse et qui n'est manifestement pas une constante :
jmp
dword
ptr
(1B1664h
)[ecx
*
4
]
Ceci dit « sauter à l'emplacement stocké dans l'adresse mémoire à un décalage de 4 fois la valeur de ecx
depuis l'adresse mémoire 8D1664h ». Comment est-ce que cela met en œuvre la logique C++ de l'instruction switch
?
Pour répondre à cette question, nous devons regarder dans une fenêtre de mémoire à l'adresse 8D1664h (NB : pour ouvrir une fenêtre mémoire depuis le menu de VS2010 lors du débogage, sélection le menu « Debug » puis « Windows » puis « Memory » et choisissez l'une des fenêtres mémoire. Pour définir l'adresse, il suffit de la copier à partir du désassembleur et de la coller dans le champ « Adresse: ». Vous devrez également faire un clic droit et choisir l'option « 4-byte integer » et définir la valeur du champ « Columns » à 1 pour lui donner le même aspect que la capture d'écran ci-dessus).
Donc, si vous portez votre regard jusqu'à la fenêtre de mémoire sur la gauche de la capture d'écran ci-dessus, vous verrez que les 4 premières lignes sont mises en évidence. Ces valeurs commencent à l'adresse 8D1664h et sont des entiers sur 4 octets (d'où le ecx
* 4 dans l'opérande) - qui précisément dans ce cas sont des pointeurs.
L'instruction « jmp
dword
ptr
(8D1664h
) [ecx
*
4
] » va sauter à la valeur stockée dans l'adresse :
8D1664h
+
0
=
8D1664h
si la valeur deecx
est 0 ;8D1664h
+
4
>=
8D1668h
si la valeur deecx
est 1 ;8D1664h
+
8
=
8D166Ch
si la valeur deecx
est 2 ;8D1664h
+
Ch
=
8D1670h
si la valeur deecx
est 3.
Ainsi, les quatre lignes ci-dessus représentent une table de saut et comme les constantes de nos case
vont de 0 à 3, c'est un tableau de 4 pointeurs. Chaque élément de ce tableau pointe vers l'adresse d'exécution du bloc case
correspondant à son indice dans la table.
Vous pouvez le vérifier en regardant l'adresse de la première instruction générée pour chaque bloc case
avec les 4 valeurs stockées dans cette table.
Peut-être que c'est juste moi, mais je pense que c'est de l'assembleur plutôt cool. Il est certainement plus élégant que l'assembleur généré par le premier switch
que nous avons examiné. Mais quel est l'avantage (s'il y en a un) de cette solution par rapport à l'exemple précédent ?
En théorie, cette forme de table de saut atteint le code en un temps constant pour tous les case
alors qu'avec la forme if
-
else
-
if
-
else
, le temps pour atteindre le code correspondant à chaque case
est proportionnel au nombre de case
précédents dans l'instruction switch
.
Il y a très peu de chances de trouver qu'une instruction switch
est un goulot d'étranglement dans votre code (sauf si vous avez fait des choses stupides), mais, toutes choses étant égales par ailleurs, l'approche par table de saut utilise moins d'instructions ce qui, normalement, est une bonne chose et qui, en théorie, devrait la rendre en moyenne plus rapide.
Un dernier mot sur les déclarations switch
, je suis sûr que, en plus du comportement linéaire de recherche dans le cas du if
-
else
-
if
-
else
pour trouver le bloc correct à exécuter, les compilateurs les plus modernes sont également capables de générer une recherche binaire dans le bloc cases des instructions switch
possédant des valeurs constantes appropriées.
Utiliser une recherche binaire plutôt qu'une recherche linéaire permet d'améliorer le temps moyen de recherche de linéaire à logarithmique (c'est-à-dire de O(n) à O(log n)). Toutefois, une recherche binaire dans une instruction switch
aura encore presque toujours plus d'instructions et de branches pour atteindre le bloc de code correct comparé à une table de sauts.
Il est également possible que le compilateur puisse choisir d'utiliser une ou plusieurs de ces méthodes dans un seul switch
. Toutefois, cela nécessiterait probablement un grand nombre de case
dans le switch
et des intervalles de constantes avec des propriétés très spécifiques, il est donc peu probable que vous ayez à en voir très souvent.
Quelques remarques supplémentaires à noter à propos des instructions switch
:
- le compilateur doit être capable de générer une table de saut indépendamment de l'ordre dans des constantes dans votre code (par exemple, cas n ° 2 : … case 1 : … case 3 : … devrait très bien fonctionner) ;
- avoir une plage de constantes pour les
case
qui commencent à 0 rend le code autour d'une table de saut plus simple, car il élimine les bornes inférieures à tester.
Une table de saut doit être créée tant que la plage des constantes est assez grande et / ou suffisamment serrée pour que le compilateur puisse se décider si cela vaut la peine, même si elles ne sont pas complètement contiguës. Regardez-le désassembleur si vous voulez vérifier.
V. Résumé▲
Donc, cela conclut notre examen sur les conditions, j'espère que vous avez trouvé cela intéressant et éclairant. ;)
Un dernier point à ne pas perdre de vue sur les conditions, c'est que tandis que le compilateur peut générer le même assembleur pour un if
-
else
que pour l'opérateur conditionnel, il ne le fait pas. De même qu'il pourrait générer le même assembleur pour un if
-
else
-
if
-
else
que pour une instruction switch
, mais il ne le fait pas non plus.
En partie, cela montre les limites du compilateur, mais montre également l'importance d'utiliser la condition appropriée pour un but donné, l'avantage est que ce que vous utilisez rend votre intention claire aux lecteurs humains de votre code.
Nous avons maintenant couvert suffisamment de terrain si bien que vous devriez pouvoir appliquer les informations que je vous ai données à des problèmes courants de programmation pour débogage du code en mode release ou du code dont vous n'avez pas les informations de débogage.
Les principales choses que je voudrais que vous preniez en considération sur les conditions sont des choses qui vous aideront lors du débogage sans symboles :
- quand vous voyez une instruction
cmp
suivie d'une instruction jxx à une adresse à proximité dans le désassembleur, vous êtes probablement en train de regarder le code généré par une instruction de condition dans le code C/C++ ; - si l'opérande adresse de l'instruction de saut est inférieure à l'adresse de l'instruction courante (c'est-à-dire qu'il y a un saut en arrière), vous êtes probablement en train de regarder une boucle ;
- l'assembleur généré par une condition teste en général à l'opposé du test spécifié dans le C/C++.
En utilisant ces méthodes heuristiques, regarder les valeurs dans les registres, les valeurs dans la Pile qui ont été écrits par l'assembleur et en consultant votre adresse actuelle dans le fichier de symboles pour savoir dans quelle fonction vous êtes (si vous ne générez pas un fichier de symboles pour tous vos projets, vous devriez le faire, regardez dans la documentation du compilateur pour savoir comment), vous devriez être en mesure de faire une supposition éclairée sur les variables dans le code C/C++ de votre plateforme susceptible d'être l'origine du problème actuel et ceci vous dira généralement pourquoi il s'est crashé ou vous donner une indication de sorte que vous pouvez jouer votre Sherlock Holmes pour remonter à la racine du problème. C'est certainement beaucoup plus rapide que l'insertion omniprésente de nombreux printf()…
Notre prochain sujet traitera des boucles, ce qui évidemment aussi utilisera des sauts conditionnels (ce qui explique pourquoi nous avons d'abord couvert les conditions…).
VI. Une chose enfin…▲
Merci à Tony, Bruce et Fabian pour leurs informations supplémentaires, leurs conseils et leur relecture.
Et, pour ceux d'entre vous qui voudraient aller plus loin (la plupart d'entre vous j'espère !), j'ai récemment découvert ce site wiki sur l'assembleur x86. Il a un grand chevauchement avec cette série d'articles et couvre également la programmation en assembleur x86. Fortement recommandé, je l'ai certainement trouvé très utile.
Finalement, une pépite finale de sagesse par Bruce Dawson :
Un autre problème que j'ai vu avec est qu'il y a des gens qui ont « intelligemment » créé des classes string qui ont à la fois un constructeur const
char
*
et un opérateur de conversion const
char
*
. Cela a le potentiel extraordinairement dangereux et inefficace de permettre de nombreuses conversions cachées. Ils comprennent alors avec ce code :
return
bFlag ? mStringObject : "Hello world"
;
La question est de connaître le type de cette expression ? Est-ce que mStringObject est converti en un const
char
*
ou bien est-ce que c'est « Bonjour tout le monde » qui est converti en string ? Je n'ai aucune idée et personne ne devrait mémoriser les règles pertinentes, ce code est trop fragile et dangereux.
Certaines personnes pourraient penser que le type de l'opérateur conditionnel dépend du type de retour de la fonction, mais ce n'est pas vrai. Ils sont indépendants. Ainsi, il est fort possible que « Bonjour tout le monde » soit converti en un objet string, puis (si le type de retour de la fonction est const
char
*
) cet objet string (temporaire !!) sera reconverti en un const
char
*
. En plus d'être inefficace, cela conduit à un comportement indéfini, puisque nous retournons un pointeur vers la mémoire appartenant à un objet qui est détruit quand la fonction se termine.
L'utilisation de l'opérateur conditionnel avec des types incompatibles est mal, mal, mal. Les classes string avec un opérateur de conversion const
char
*
est mal, mal, mal. Les mettre ensemble… cela n'a pas de prix.
VII. Remerciements▲
Cet article est la traduction de l'article « C / C++ Low Level Curriculum Part 7: More Conditionals » écrit en anglais par Alex Darby. Alex Darby a aimablement autorisé l'équipe C/C++ de Developpez.com à traduire et diffuser son article en français.
Je tiens à remercier zoom61 pour la relecture orthographique de cette traduction.