Pourquoi ces constructions utilisent-elles un comportement non défini avant et après incrémentation?

815
#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d\n", ++w, w); // shouldn't this print 1 1

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
PiX
la source
12
@ Jarett, non, juste besoin de quelques pointeurs pour "séquencer les points". En travaillant, j'ai trouvé un morceau de code avec i = i ++, je pensais que "cela ne modifie pas la valeur de i". J'ai testé et je me suis demandé pourquoi. Depuis, j'ai supprimé cet énoncé et l'ai remplacé par i ++;
PiX
198
Je pense qu'il est intéressant que tout le monde suppose TOUJOURS que des questions comme celle-ci sont posées parce que le demandeur veut UTILISER la construction en question. Ma première hypothèse était que PiX sait que ce sont mauvais, mais est curieux de savoir pourquoi ils se comportent comme ils le font sur ce que le compilateur a utilisé ... Et oui, ce que UnWind a dit ... il n'est pas défini, il pourrait faire n'importe quoi. .. y compris JCF (Jump and Catch Fire)
Brian Postow
32
Je suis curieux: pourquoi les compilateurs ne semblent-ils pas mettre en garde contre des constructions telles que "u = u ++ + ++ u;" si le résultat n'est pas défini?
Apprenez OpenGL ES
5
(i++)évalue toujours à 1, sans tenir compte des parenthèses
Drew McGowen
2
Quoi qu'il en i = (i++);soit, il existe certainement un moyen plus clair de l'écrire. Ce serait vrai même s'il était bien défini. Même en Java, qui définit le comportement de i = (i++);, c'est toujours du mauvais code. Il suffit d'écrirei++;
Keith Thompson

Réponses:

566

C a le concept de comportement non défini, c'est-à-dire que certaines constructions de langage sont syntaxiquement valides mais vous ne pouvez pas prédire le comportement lorsque le code est exécuté.

Pour autant que je sache, la norme ne dit pas explicitement pourquoi le concept de comportement indéfini existe. Dans mon esprit, c'est simplement parce que les concepteurs de langage voulaient qu'il y ait une certaine latitude dans la sémantique, au lieu d'exiger que toutes les implémentations gèrent le débordement d'entier de la même manière, ce qui imposerait très probablement de sérieux coûts de performance, ils ont juste laissé le comportement non défini de sorte que si vous écrivez du code qui provoque un débordement d'entier, tout peut arriver.

Donc, dans cet esprit, pourquoi ces «problèmes»? Le langage dit clairement que certaines choses conduisent à un comportement indéfini . Il n'y a pas de problème, il n'y a pas de "devrait" impliqué. Si le comportement non défini change lorsqu'une des variables impliquées est déclarée volatile, cela ne prouve ni ne change rien. Ce n'est pas défini ; vous ne pouvez pas raisonner sur le comportement.

Votre exemple le plus intéressant, celui avec

u = (u++);

est un exemple de manuel de comportement indéfini (voir l'entrée de Wikipedia sur les points de séquence ).

se détendre
la source
8
@PiX: Les choses ne sont pas définies pour un certain nombre de raisons possibles. Ceux-ci comprennent: il n'y a pas de "bon résultat" clair, différentes architectures de machine favoriseraient fortement des résultats différents, la pratique existante n'est pas cohérente, ou dépasse le cadre de la norme (par exemple, quels noms de fichiers sont valides).
Richard
Juste pour confondre tout le monde, certains de ces exemples sont maintenant bien définis dans C11, par exemple i = ++i + 1;.
MM
2
En lisant la norme et la justification publiée, il est clair pourquoi le concept d'UB existe. La norme n'a jamais été conçue pour décrire complètement tout ce qu'une implémentation C doit faire pour convenir à un usage particulier (voir la discussion de la règle du "programme unique"), mais s'appuie plutôt sur le jugement des implémenteurs et leur désir de produire des implémentations de qualité utiles. Une implémentation de qualité adaptée à la programmation de systèmes de bas niveau devra définir le comportement des actions qui ne seraient pas nécessaires dans les applications crunching.applications numériques haut de gamme. Plutôt que d'essayer de compliquer le Standard ...
supercat
3
... en entrant dans les détails extrêmes sur les cas d'angle qui sont ou ne sont pas définis, les auteurs de la norme ont reconnu que les implémenteurs devraient être mieux placés pour juger quels types de comportements seront nécessaires par les types de programmes qu'ils sont censés prendre en charge . Les compilateurs hyper-modernistes prétendent que faire certaines actions UB était destiné à impliquer qu'aucun programme de qualité ne devrait en avoir besoin, mais la norme et la justification sont incompatibles avec une telle intention supposée.
supercat
1
@jrh: J'ai écrit cette réponse avant d'avoir réalisé à quel point la philosophie hyper-moderniste était devenue incontrôlable. Ce qui me contrarie, c'est la progression de "Nous n'avons pas besoin de reconnaître officiellement ce comportement car les plates-formes où il est nécessaire peuvent le prendre en charge de toute façon" à "Nous pouvons supprimer ce comportement sans fournir un remplacement utilisable car il n'a jamais été reconnu et donc aucun code en avoir besoin était cassé ". De nombreux comportements auraient dû être déconseillés il y a longtemps au profit de remplacements qui étaient à tous points de vue meilleurs , mais cela aurait nécessité de reconnaître leur légitimité.
supercat
78

Compilez et démontez simplement votre ligne de code, si vous êtes si enclin à savoir exactement comment vous obtenez ce que vous obtenez.

Voici ce que j'obtiens sur ma machine, avec ce que je pense se passe:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(Je ... suppose que l'instruction 0x00000014 était une sorte d'optimisation du compilateur?)

badp
la source
comment obtenir le code machine? J'utilise Dev C ++, et j'ai joué avec l'option 'Génération de code' dans les paramètres du compilateur, mais je
n'effectue
5
@ronnieaka gcc evil.c -c -o evil.binet gdb evil.bindisassemble evil, ou quels que soient leurs équivalents Windows :)
badp
21
Cette réponse ne répond pas vraiment à la question de Why are these constructs undefined behavior?.
Shafik Yaghmour du
9
En passant, il sera plus facile de compiler en assemblage (avec gcc -S evil.c), ce qui est tout ce qui est nécessaire ici. Le monter puis le démonter n'est qu'un moyen détourné de le faire.
Kat
50
Pour mémoire, si pour une raison quelconque vous vous demandez ce que fait une construction donnée - et surtout s'il y a un soupçon qu'il pourrait s'agir d'un comportement indéfini - le conseil séculaire de "essayez-le avec votre compilateur et voyez" est potentiellement assez périlleux. Vous apprendrez, au mieux, ce qu'il fait sous cette version de votre compilateur, dans ces circonstances, aujourd'hui . Vous n'apprendrez pas grand-chose sur ce qu'il est garanti de faire. En général, "essayez simplement avec votre compilateur" conduit à des programmes non portables qui ne fonctionnent qu'avec votre compilateur.
Steve Summit
64

Je pense que les parties pertinentes de la norme C99 sont 6.5 Expressions, §2

Entre le point de séquence précédent et suivant, un objet doit voir sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression. En outre, la valeur antérieure doit être lue uniquement pour déterminer la valeur à stocker.

et 6.5.16 Opérateurs d'affectation, §4:

L'ordre d'évaluation des opérandes n'est pas spécifié. Si une tentative est effectuée pour modifier le résultat d'un opérateur d'affectation ou pour y accéder après le point de séquence suivant, le comportement n'est pas défini.

Christoph
la source
2
Est-ce que ce qui précède impliquerait que 'i = i = 5; "serait un comportement
indéfini
1
@supercat pour autant que je sache, i=i=5c'est aussi un comportement indéfini
dhein
2
@Zaibis: La logique que j'aime utiliser pour la plupart des endroits s'applique qu'en théorie une plate-forme multiprocesseur pourrait implémenter quelque chose comme A=B=5;"Write-lock A; Write-Lock B; Store 5 to A; store 5 to B; Unlock B ; Déverrouiller A; ", et une instruction telle C=A+B;que" Verrou de lecture A; Verrou de lecture B; Calculer A + B; Déverrouiller A et B; Verrou d'écriture C; Stocker le résultat; Déverrouiller C; ". Cela garantirait que si un thread le faisait A=B=5;tandis qu'un autre le faisait, C=A+B;ce dernier thread verrait les deux écritures avoir eu lieu ou aucun. Potentiellement une garantie utile. Si un fil l'a fait I=I=5;, cependant ...
supercat
1
... et le compilateur n'a pas remarqué que les deux écritures étaient au même emplacement (si une ou les deux valeurs impliquent des pointeurs, cela peut être difficile à déterminer), le code généré pourrait se bloquer. Je ne pense pas que les implémentations du monde réel implémentent un tel verrouillage dans le cadre de leur comportement normal, mais ce serait autorisé par la norme, et si le matériel pouvait implémenter de tels comportements à moindre coût, il pourrait être utile. Sur le matériel actuel, un tel comportement serait beaucoup trop coûteux à implémenter par défaut, mais cela ne signifie pas qu'il en serait toujours ainsi.
supercat
1
@supercat mais la règle d'accès au point de séquence de c99 ne suffirait-elle pas à elle seule à le déclarer comme un comportement non défini? Donc, peu importe ce que le matériel pourrait implémenter techniquement?
dhein
55

La plupart des réponses citées ici de la norme C soulignent que le comportement de ces constructions n'est pas défini. Pour comprendre pourquoi le comportement de ces constructions n'est pas défini , comprenons d'abord ces termes à la lumière de la norme C11:

Séquencé: (5.1.2.3)

Étant donné deux évaluations quelconques Aet B, s'il Aest séquencé auparavant B, l'exécution de Adoit précéder l'exécution de B.

Non séquencé:

Si An'est pas séquencé avant ou après B, alors Aet ne Bsont pas séquencés.

Les évaluations peuvent être l'une des deux choses suivantes:

  • calculs de valeur , qui déterminent le résultat d'une expression; et
  • les effets secondaires , qui sont des modifications d'objets.

Point de séquence:

La présence d'un point de séquence entre l'évaluation des expressions Aet Bimplique que chaque calcul de valeur et effet secondaire associé à Aest séquencé avant chaque calcul de valeur et effet secondaire associé à B.

Venons-en maintenant à la question, pour les expressions comme

int i = 1;
i = i++;

la norme dit que:

6.5 Expressions:

Si un effet secondaire sur un objet scalaire est non séquencée par rapport à soit un effet secondaire différent sur le même objet scalaire ou un calcul de valeur en utilisant la valeur d'un même objet scalaire, le comportement est indéfini . [...]

Par conséquent, l'expression ci-dessus appelle UB car deux effets secondaires sur le même objet ne isont pas séquencés l'un par rapport à l'autre. Cela signifie qu'il n'est pas séquencé si l'effet secondaire par affectation à isera effectué avant ou après l'effet secondaire par ++.
Selon que l'affectation se produit avant ou après l'incrément, différents résultats seront produits et c'est le cas du comportement indéfini .

Permet de renommer l'être ià gauche de l'affectation ilet à droite de l'affectation (dans l'expression i++) ir, puis l'expression soit comme

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Un point important concernant l' ++opérateur Postfix est que:

ce n'est pas parce que ++vient après la variable que l'incrément se produit tard . L'incrémentation peut se produire dès que le compilateur le souhaite tant que le compilateur s'assure que la valeur d'origine est utilisée .

Cela signifie que l'expression il = ir++pourrait être évaluée soit comme

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

ou

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

résultant en deux résultats différents 1et 2qui dépend de la séquence d'effets secondaires par affectation ++et donc appelle UB.

haccks
la source
52

Le comportement ne peut pas vraiment être expliqué car il invoque à la fois un comportement non spécifié et un comportement indéfini , donc nous ne pouvons pas faire de prédictions générales sur ce code, bien que si vous lisez le travail d' Olve Maudal tel que Deep C et Unspecified and Undefined, parfois vous pouvez faire du bien devine dans des cas très spécifiques avec un compilateur et un environnement spécifiques, mais ne le faites pas à proximité de la production.

Donc, pour passer à un comportement non spécifié , dans le projet de norme C99 , le 6.5paragraphe 3 dit ( soulignement le mien ):

Le regroupement d'opérateurs et d'opérandes est indiqué par la syntaxe.74) Sauf comme spécifié plus loin (pour les opérateurs de fonction call (), &&, ||,?: Et virgule), l'ordre d'évaluation des sous-expressions et l'ordre dans quels effets secondaires se produisent ne sont pas spécifiés.

Donc, quand nous avons une ligne comme celle-ci:

i = i++ + ++i;

nous ne savons pas si i++ou ++iseront évalués en premier. C'est principalement pour donner au compilateur de meilleures options d'optimisation .

Nous avons aussi un comportement non défini ici aussi puisque le programme est en train de modifier les variables ( i, u, etc ..) plus d'une fois entre les points de séquence . Extrait du projet de section standard, 6.5paragraphe 2 ( je souligne ):

Entre le point de séquence précédent et suivant, un objet doit voir sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression. En outre, la valeur antérieure doit être lue uniquement pour déterminer la valeur à stocker .

il cite les exemples de code suivants comme non définis:

i = ++i + 1;
a[i++] = i; 

Dans tous ces exemples, le code tente de modifier un objet plusieurs fois dans le même point de séquence, ce qui se terminera par ;dans chacun de ces cas:

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

Le comportement non spécifié est défini dans le projet de norme c99 dans la section 3.4.4:

utilisation d'une valeur non spécifiée ou d'un autre comportement lorsque la présente Norme internationale offre deux possibilités ou plus et n'impose aucune autre exigence sur laquelle est choisie dans tous les cas

et le comportement indéfini est défini dans la section 3.4.3comme:

comportement, lors de l'utilisation d'une construction de programme non portable ou erronée ou de données erronées, pour laquelle la présente Norme internationale n'impose aucune exigence

et note que:

Les comportements indéfinis possibles vont de l'ignorance complète de la situation avec des résultats imprévisibles, au comportement pendant la traduction ou l'exécution du programme d'une manière documentée caractéristique de l'environnement (avec ou sans l'émission d'un message de diagnostic), à la fin d'une traduction ou de l'exécution (avec l'émission d'un message de diagnostic).

Shafik Yaghmour
la source
33

Une autre façon de répondre à cela, plutôt que de s'enliser dans les détails obscurs des points de séquence et des comportements indéfinis, consiste simplement à demander, que sont-ils censés signifier? Qu'est-ce que le programmeur essayait de faire?

Le premier fragment demandé i = i++ + ++i, est assez clairement fou dans mon livre. Personne ne l'écrirait jamais dans un vrai programme, ce n'est pas évident, il n'y a pas d'algorithme concevable que quelqu'un aurait pu essayer de coder qui aurait entraîné cette séquence d'opérations artificielle particulière. Et comme ce n'est pas évident pour vous et moi ce qu'il est censé faire, c'est bien dans mon livre si le compilateur ne peut pas comprendre ce qu'il est censé faire non plus.

Le deuxième fragment,, i = i++est un peu plus facile à comprendre. Quelqu'un essaie clairement d'incrémenter i et d'assigner le résultat à i. Mais il y a plusieurs façons de le faire en C. La façon la plus simple d'ajouter 1 à i et de réattribuer le résultat à i est la même dans presque tous les langages de programmation:

i = i + 1

C, bien sûr, a un raccourci pratique:

i++

Cela signifie "ajouter 1 à i et attribuer le résultat à i". Donc, si nous construisons un méli-mélo des deux, en écrivant

i = i++

ce que nous disons vraiment, c'est "ajouter 1 à i, et attribuer le résultat à i, et attribuer le résultat à i". Nous sommes confus, donc cela ne me dérange pas trop si le compilateur est également confus.

De façon réaliste, la seule fois où ces expressions folles sont écrites, c'est lorsque les gens les utilisent comme des exemples artificiels de la façon dont ++ est censé fonctionner. Et bien sûr, il est important de comprendre comment ++ fonctionne. Mais une règle pratique pour l'utilisation de ++ est: "Si ce n'est pas évident ce que signifie une expression utilisant ++, ne l'écrivez pas."

Nous avions l'habitude de passer d'innombrables heures sur comp.lang.c à discuter d'expressions comme celles-ci et pourquoi elles n'étaient pas définies. Deux de mes réponses plus longues, qui tentent d'expliquer vraiment pourquoi, sont archivées sur le Web:

Voir aussi question 3.8 et le reste des questions à l' article 3 de la liste C FAQ .

Sommet Steve
la source
1
Un Gotcha plutôt désagréable à l' égard du comportement non défini est que bien qu'il utilisé pour être sûr à 99,9% des compilateurs à utiliser *p=(*q)++;pour signifier if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;que n'est plus le cas. Le C hyper-moderne nécessiterait d'écrire quelque chose comme cette dernière formulation (bien qu'il n'y ait pas de moyen standard d'indiquer que le code ne se soucie pas de ce qu'il contient *p) pour atteindre le niveau d'efficacité des compilateurs utilisés pour fournir le premier (la elseclause est nécessaire afin de laisser le compilateur optimise ce ifdont certains compilateurs plus récents auraient besoin).
supercat
@supercat Je crois maintenant que tout compilateur suffisamment «intelligent» pour effectuer ce type d'optimisation doit également être suffisamment intelligent pour jeter un œil aux assertinstructions, afin que le programmeur puisse précéder la ligne en question avec un simple assert(p != q). (Bien sûr, suivre ce cours nécessiterait également une réécriture <assert.h>pour ne pas supprimer purement et simplement les assertions dans les versions non déboguées, mais plutôt les transformer en quelque chose comme __builtin_assert_disabled()le compilateur lui-même peut voir, puis ne pas émettre de code pour.)
Steve Summit
25

Souvent, cette question est liée comme un doublon de questions liées au code comme

printf("%d %d\n", i, i++);

ou

printf("%d %d\n", ++i, i++);

ou des variantes similaires.

Bien qu'il s'agisse également d' un comportement indéfini, comme indiqué précédemment, il existe de subtiles différences lorsqu'il printf()est impliqué lors de la comparaison avec une déclaration telle que:

x = i++ + i++;

Dans la déclaration suivante:

printf("%d %d\n", ++i, i++);

l' ordre d'évaluation des arguments dans printf()n'est pas spécifié . Cela signifie que les expressions i++et ++ipourraient être évaluées dans n'importe quel ordre. La norme C11 contient quelques descriptions pertinentes à ce sujet:

Annexe J, comportements non spécifiés

L'ordre dans lequel l'indicateur de fonction, les arguments et les sous-expressions dans les arguments sont évalués dans un appel de fonction (6.5.2.2).

3.4.4, comportement non spécifié

Utilisation d'une valeur non spécifiée ou d'un autre comportement lorsque la présente Norme internationale offre deux possibilités ou plus et n'impose aucune autre exigence sur laquelle est choisie dans tous les cas.

EXEMPLE Un exemple de comportement non spécifié est l'ordre dans lequel les arguments d'une fonction sont évalués.

Le comportement non spécifié lui-même n'est PAS un problème. Considérez cet exemple:

printf("%d %d\n", ++x, y++);

Cela aussi a un comportement non spécifié car l'ordre d'évaluation de ++xet y++n'est pas spécifié. Mais c'est une déclaration parfaitement légale et valide. Il n'y a aucun comportement indéfini dans cette déclaration. Parce que les modifications ( ++xet y++) sont effectuées sur des objets distincts .

Ce qui rend la déclaration suivante

printf("%d %d\n", ++i, i++);

comme comportement indéfini est le fait que ces deux expressions modifient le même objet isans point de séquence intermédiaire .


Un autre détail est que la virgule impliquée dans l'appel printf () est un séparateur , pas l' opérateur virgule .

Il s'agit d'une distinction importante car l' opérateur virgule introduit un point de séquence entre l'évaluation de leurs opérandes, ce qui rend les éléments suivants légaux:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

L'opérateur virgule évalue ses opérandes de gauche à droite et ne donne que la valeur du dernier opérande. Donc j = (++i, i++);, par ++iincréments ide 6et i++rendements ancienne valeur i( 6) qui est attribué à j. Devient ialors 7dû au post-incrément.

Donc, si la virgule dans l'appel de fonction devait être un opérateur de virgule,

printf("%d %d\n", ++i, i++);

ne sera pas un problème. Mais il invoque un comportement indéfini car la virgule est ici un séparateur .


Pour ceux qui découvrent un comportement indéfini, il serait avantageux de lire ce que tout programmeur C devrait savoir sur le comportement indéfini pour comprendre le concept et de nombreuses autres variantes de comportement indéfini en C.

Ce message: Un comportement non défini, non spécifié et défini par la mise en œuvre est également pertinent.

PP
la source
Cette séquence int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));semble donner un comportement stable (évaluation des arguments de droite à gauche dans gcc v7.3.0; résultat "a = 110 b = 40 c = 60"). Est-ce parce que les affectations sont considérées comme des «déclarations complètes» et introduisent ainsi un point de séquence? Cela ne devrait-il pas se traduire par une évaluation des arguments / déclarations de gauche à droite? Ou s'agit-il simplement d'une manifestation d'un comportement indéfini?
kavadias
@kavadias Cette instruction printf implique un comportement indéfini, pour la même raison expliquée ci-dessus. Vous écrivez bet cdans les arguments 3ème et 4ème respectivement , et la lecture dans le 2ème argument. Mais il n'y a pas de séquence entre ces expressions (2e, 3e et 4e arguments). gcc / clang a une option -Wsequence-pointqui peut aussi aider à les trouver.
PP
23

Bien qu'il soit peu probable que des compilateurs et des processeurs le fassent réellement, il serait légal, selon la norme C, que le compilateur implémente "i ++" avec la séquence:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

Bien que je ne pense pas que les processeurs prennent en charge le matériel pour permettre qu'une telle chose soit faite efficacement, on peut facilement imaginer des situations où un tel comportement rendrait le code multithread plus facile (par exemple, cela garantirait que si deux threads essayaient d'effectuer ce qui précède) séquence simultanément, iserait incrémenté de deux) et il n'est pas totalement inconcevable que certains futurs processeurs fournissent une fonctionnalité similaire.

Si le compilateur devait écrire i++comme indiqué ci-dessus (légal en vertu de la norme) et intercaler les instructions ci-dessus tout au long de l'évaluation de l'expression globale (également légal), et s'il ne s'est pas avéré que l'une des autres instructions s'est produite pour y accéder i, il serait possible (et légal) pour le compilateur de générer une séquence d'instructions qui se bloquerait. Pour être sûr, un compilateur détecterait presque certainement le problème dans le cas où la même variable iest utilisée aux deux endroits, mais si une routine accepte des références à deux pointeurs pet q, et utilise (*p)et (*q)dans l'expression ci-dessus (plutôt que d'utiliserideux fois), le compilateur ne serait pas tenu de reconnaître ou d'éviter le blocage qui se produirait si la même adresse d'objet était transmise pour les deux .p etq

supercat
la source
16

Bien que la syntaxe des expressions comme a = a++ou a++ + a++soit légale, le comportement de ces constructions n'est pas défini car un devoir en standard C n'est pas respecté. C99 6.5p2 :

  1. Entre le point de séquence précédent et suivant, un objet doit voir sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression. [72] En outre, la valeur antérieure doit être lue uniquement pour déterminer la valeur à stocker [73]

La note de bas de page 73 précisant davantage que

  1. Ce paragraphe rend des expressions d'instruction non définies telles que

    i = ++i + 1;
    a[i++] = i;

    tout en permettant

    i = i + 1;
    a[i] = i;

Les différents points de séquence sont énumérés à l'annexe C de C11 (et C99 ):

  1. Voici les points de séquence décrits au 5.1.2.3:

    • Entre les évaluations de l'indicateur de fonction et les arguments réels dans un appel de fonction et l'appel réel. (6.5.2.2).
    • Entre les évaluations des premier et deuxième opérandes des opérateurs suivants: AND logique && (6.5.13); OU logique || (6.5.14); virgule, (6.5.17).
    • Entre les évaluations du premier opérande du conditionnel? : opérateur et celui des deuxième et troisième opérandes est évalué (6.5.15).
    • La fin d'un déclarant complet: déclarateurs (6.7.6);
    • Entre l'évaluation d'une expression complète et la prochaine expression complète à évaluer. Les expressions suivantes sont complètes: un initialiseur qui ne fait pas partie d'un littéral composé (6.7.9); l'expression dans une instruction d'expression (6.8.3); l'expression de contrôle d'une instruction de sélection (if ou switch) (6.8.4); l'expression de contrôle d'une instruction while ou do (6.8.5); chacune des expressions (facultatives) d'une instruction for (6.8.5.3); l'expression (facultative) dans une instruction de retour (6.8.6.4).
    • Immédiatement avant le retour d'une fonction de bibliothèque (7.1.4).
    • Après les actions associées à chaque spécificateur de conversion de fonction d'entrée / sortie formatée (7.21.6, 7.29.2).
    • Immédiatement avant et immédiatement après chaque appel à une fonction de comparaison, et également entre tout appel à une fonction de comparaison et tout mouvement des objets passés en arguments à cet appel (7.22.5).

Le libellé du même paragraphe dans C11 est le suivant:

  1. Si un effet secondaire sur un objet scalaire n'est pas séquencé par rapport à un effet secondaire différent sur le même objet scalaire ou à un calcul de valeur utilisant la valeur du même objet scalaire, le comportement n'est pas défini. S'il existe plusieurs ordres autorisés des sous-expressions d'une expression, le comportement n'est pas défini si un tel effet secondaire non séquencé se produit dans l'un des ordres.84)

Vous pouvez détecter de telles erreurs dans un programme en utilisant par exemple une version récente de GCC avec -Wallet -Werror, puis GCC refusera carrément de compiler votre programme. Ce qui suit est la sortie de gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function main’:
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

La partie importante est de savoir ce qu'est un point de séquence - et qu'est-ce qu'un point de séquence et ce qui ne l'est pas . Par exemple, l' opérateur virgule est un point de séquence, donc

j = (i ++, ++ i);

est bien défini et augmentera i d'une unité, ce qui l'ancienne valeur, rejetez cette valeur; puis chez l'opérateur virgule, régler les effets secondaires; puis incrémenter ide un, et la valeur résultante devient la valeur de l'expression - c'est-à-dire que c'est juste une façon artificielle d'écrire j = (i += 2)qui est encore une fois une façon "intelligente" d'écrire

i += 2;
j = i;

Cependant, la ,liste des arguments de la fonction est pas un opérateur virgule et il n'y a pas de point de séquence entre les évaluations d'arguments distincts; au lieu de cela, leurs évaluations ne sont pas séquencées les unes par rapport aux autres; donc l'appel de fonction

int i = 0;
printf("%d %d\n", i++, ++i, i);

a un comportement indéfini car il n'y a pas de point de séquence entre les évaluations de i++et ++idans les arguments de fonction , et la valeur de iest donc modifiée deux fois, par les deux i++et ++i, entre le point de séquence précédent et le suivant.

Antti Haapala
la source
14

La norme C indique qu'une variable ne doit être affectée qu'au plus une fois entre deux points de séquence. Un point-virgule, par exemple, est un point de séquence.
Donc, chaque déclaration du formulaire:

i = i++;
i = i++ + ++i;

et ainsi de suite violer cette règle. La norme indique également que le comportement n'est pas défini et n'est pas spécifié. Certains compilateurs les détectent et produisent des résultats, mais ce n'est pas conforme à la norme.

Cependant, deux variables différentes peuvent être incrémentées entre deux points de séquence.

while(*src++ = *dst++);

Ce qui précède est une pratique de codage courante lors de la copie / analyse de chaînes.

Nikhil Vidhani
la source
Bien sûr, cela ne s'applique pas aux différentes variables d'une même expression. Ce serait un échec de conception total si c'était le cas! Tout ce dont vous avez besoin dans le 2ème exemple est que les deux soient incrémentés entre la fin de l'instruction et le début suivant, et c'est garanti, précisément en raison du concept de points de séquence au centre de tout cela.
underscore_d
11

Dans /programming/29505280/incrementing-array-index-in-c, quelqu'un a posé des questions sur une déclaration comme:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

qui imprime 7 ... l'OP s'attendait à ce qu'il imprime 6.

Les ++iincréments ne sont pas garantis d'être tous terminés avant le reste des calculs. En fait, différents compilateurs obtiendront des résultats différents ici. Dans l'exemple que vous avez fourni, le premier 2 ++iexécuté, les valeurs de k[]ont été lus, la dernière ++ialors k[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3

Les compilateurs modernes optimiseront très bien cela. En fait, peut-être mieux que le code que vous avez écrit à l'origine (en supposant qu'il a fonctionné comme vous l'aviez espéré).

TomOnTime
la source
5

Une bonne explication de ce qui se passe dans ce type de calcul est fournie dans le document n1188 du site ISO W14 .

J'explique les idées.

La règle principale de la norme ISO 9899 qui s'applique dans cette situation est 6.5p2.

Entre le point de séquence précédent et suivant, un objet doit voir sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression. En outre, la valeur antérieure doit être lue uniquement pour déterminer la valeur à stocker.

Les points de séquence dans une expression comme i=i++sont avant i=et après i++.

Dans le document que j'ai cité ci-dessus, il est expliqué que vous pouvez comprendre que le programme est formé de petites cases, chaque case contenant les instructions entre 2 points de séquence consécutifs. Les points de séquence sont définis à l'annexe C de la norme, dans le cas dei=i++ il y a 2 points de séquence qui délimitent une expression complète. Une telle expression est syntaxiquement équivalente à une entrée deexpression-statement sous la forme Backus-Naur de la grammaire (une grammaire est fournie dans l'annexe A de la norme).

Ainsi, l'ordre des instructions à l'intérieur d'une boîte n'a pas d'ordre clair.

i=i++

peut être interprété comme

tmp = i
i=i+1
i = tmp

ou comme

tmp = i
i = tmp
i=i+1

parce que toutes ces formes pour interpréter le code i=i++ sont valides et parce que les deux génèrent des réponses différentes, le comportement n'est pas défini.

Ainsi, un point de séquence peut être vu par le début et la fin de chaque boîte qui compose le programme [les boîtes sont des unités atomiques en C] et à l'intérieur d'une boîte, l'ordre des instructions n'est pas défini dans tous les cas. Changer cet ordre peut parfois changer le résultat.

ÉDITER:

Une autre bonne source pour expliquer ces ambiguïtés sont les entrées du site c-faq (également publié sous forme de livre ), à savoir ici et ici et ici .

alinsoar
la source
Comment cette réponse a-t-elle ajouté de nouvelles aux réponses existantes? De plus, les explications de i=i++sont très similaires à cette réponse .
haccks
@haccks Je n'ai pas lu les autres réponses. Je voulais expliquer dans ma propre langue ce que j'ai appris du document mentionné sur le site officiel de ISO 9899 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
alinsoar
5

Votre question n'était probablement pas: "Pourquoi ces constructions ont-elles un comportement indéfini en C?". Votre question était probablement: "Pourquoi ce code (en utilisant ++) ne m'a- t-il pas donné la valeur que j'attendais?", Et quelqu'un a marqué votre question en double et vous a envoyé ici.

Cette réponse tente de répondre à cette question: pourquoi votre code ne vous a-t-il pas donné la réponse que vous attendiez, et comment pouvez-vous apprendre à reconnaître (et éviter) les expressions qui ne fonctionneront pas comme prévu.

Je suppose que vous avez déjà entendu la définition de base des C ++et des --opérateurs, et comment la forme du préfixe ++xdiffère de la forme du postfix x++. Mais ces opérateurs sont difficiles à penser, alors pour vous assurer que vous avez compris, vous avez peut-être écrit un tout petit programme de test impliquant quelque chose comme

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

Mais, à votre grande surprise, ce programme ne vous a pas aidé à comprendre - il a imprimé une sortie étrange, inattendue, inexplicable, suggérant que peut-être++ fait quelque chose de complètement différent, pas du tout ce que vous pensiez qu'il faisait.

Ou, peut-être que vous regardez une expression difficile à comprendre comme

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

Peut-être que quelqu'un vous a donné ce code comme un puzzle. Ce code n'a également aucun sens, surtout si vous l'exécutez - et si vous le compilez et l'exécutez sous deux compilateurs différents, vous obtiendrez probablement deux réponses différentes! Qu'est-ce qui se passe? Quelle réponse est correcte? (Et la réponse est que les deux le sont, ou aucun d'eux ne l'est.)

Comme vous l'avez déjà entendu, toutes ces expressions ne sont pas définies , ce qui signifie que le langage C ne garantit pas ce qu'elles feront. C'est un résultat étrange et surprenant, car vous pensiez probablement que tout programme que vous pourriez écrire, tant qu'il serait compilé et exécuté, générerait une sortie unique et bien définie. Mais dans le cas d'un comportement indéfini, ce n'est pas le cas.

Qu'est-ce qui rend une expression indéfinie? Les expressions impliquant ++et-- toujours indéfinies? Bien sûr que non: ce sont des opérateurs utiles, et si vous les utilisez correctement, ils sont parfaitement bien définis.

Pour les expressions dont nous parlons, ce qui les rend indéfinies, c'est quand il y a trop de choses à la fois, quand nous ne savons pas dans quel ordre les choses vont se passer, mais quand l'ordre compte pour le résultat que nous obtenons.

Revenons aux deux exemples que j'ai utilisés dans cette réponse. Quand j'ai écrit

printf("%d %d %d\n", x, ++x, x++);

la question est, avant d'appeler printf, le compilateur calcule-t-il la valeur dex premier, ou x++, ou peut ++x- être ? Mais il s'avère que nous ne savons pas . Il n'y a pas de règle en C qui dit que les arguments d'une fonction sont évalués de gauche à droite, de droite à gauche ou dans un autre ordre. Donc , nous ne pouvons pas dire si le compilateur fera d' xabord, puis ++x, puis x++, ou x++alors ++xalors x, ou d' un autre ordre. Mais l'ordre est clairement important, car selon l'ordre utilisé par le compilateur, nous obtiendrons clairement des résultats différents imprimés par printf.

Et cette expression folle?

x = x++ + ++x;

Le problème avec cette expression est qu'elle contient trois tentatives différentes pour modifier la valeur de x: (1) la x++partie essaie d'ajouter 1 à x, de stocker la nouvelle valeur dans xet de renvoyer l'ancienne valeur de x; (2) la ++xpartie essaie d'ajouter 1 à x, de stocker la nouvelle valeur dans xet de renvoyer la nouvelle valeur de x; et (3) la x =partie essaie de réattribuer la somme des deux autres à x. Laquelle de ces trois tentatives de mission "gagnera"? À laquelle des trois valeurs sera effectivement attribuée x? Encore une fois, et peut-être de façon surprenante, il n'y a pas de règle en C pour nous le dire.

Vous pourriez imaginer que la priorité ou l'associativité ou l'évaluation de gauche à droite vous indique dans quel ordre les choses se produisent, mais ce n'est pas le cas. Vous ne me croyez peut-être pas, mais croyez-moi, je le redis: la priorité et l'associativité ne déterminent pas tous les aspects de l'ordre d'évaluation d'une expression en C. En particulier, si au sein d'une même expression, il y en a plusieurs différents endroits où nous essayons d'assigner une nouvelle valeur à quelque chose comme x, la priorité et associativité ne pas nous dire que ces tentatives se premier ou dernier, ou quoi que ce soit.


Donc, avec tout cet arrière-plan et cette introduction à l'écart, si vous voulez vous assurer que tous vos programmes sont bien définis, quelles expressions pouvez-vous écrire et lesquelles ne pouvez-vous pas écrire?

Ces expressions sont toutes très bien:

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

Ces expressions sont toutes indéfinies:

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

Et la dernière question est, comment pouvez-vous dire quelles expressions sont bien définies et quelles expressions ne sont pas définies?

Comme je l'ai dit plus tôt, les expressions non définies sont celles où il y a trop de choses à la fois, où vous ne pouvez pas être sûr de l'ordre dans lequel les choses se passent et où l'ordre est important:

  1. S'il y a une variable qui est modifiée (affectée à) à deux endroits différents ou plus, comment savez-vous quelle modification se produit en premier?
  2. S'il y a une variable qui est modifiée à un endroit et dont la valeur est utilisée à un autre endroit, comment savez-vous si elle utilise l'ancienne ou la nouvelle valeur?

Comme exemple de # 1, dans l'expression

x = x++ + ++x;

il y a trois tentatives pour modifier `x.

Comme exemple de # 2, dans l'expression

y = x + x++;

nous utilisons tous les deux la valeur de x et la modifions.

Voilà donc la réponse: assurez-vous que dans n'importe quelle expression que vous écrivez, chaque variable est modifiée au plus une fois, et si une variable est modifiée, vous n'essayez pas également d'utiliser la valeur de cette variable ailleurs.

Sommet Steve
la source
3

La raison en est que le programme exécute un comportement non défini. Le problème réside dans l'ordre d'évaluation, car aucun point de séquence n'est requis selon la norme C ++ 98 (aucune opération n'est séquencée avant ou après une autre selon la terminologie C ++ 11).

Cependant, si vous vous en tenez à un seul compilateur, vous trouverez le comportement persistant, tant que vous n'ajoutez pas d'appels de fonction ou de pointeurs, ce qui rendrait le comportement plus compliqué.

  • Donc d'abord le GCC: En utilisant Nuwen MinGW 15 GCC 7.1, vous obtiendrez:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2

    }

Comment fonctionne GCC? il évalue les sous-expressions dans un ordre de gauche à droite pour le côté droit (RHS), puis attribue la valeur au côté gauche (LHS). C'est exactement ainsi que Java et C # se comportent et définissent leurs normes. (Oui, le logiciel équivalent en Java et C # a des comportements définis). Il évalue chaque sous-expression une par une dans la déclaration RHS dans un ordre de gauche à droite; pour chaque sous-expression: le ++ c (pré-incrément) est évalué d'abord puis la valeur c est utilisée pour l'opération, puis le post-incrément c ++).

selon GCC C ++: Opérateurs

Dans GCC C ++, la priorité des opérateurs contrôle l'ordre dans lequel les opérateurs individuels sont évalués

le code équivalent en comportement défini C ++ tel que GCC le comprend:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

    volatile int u = 0;
    //u = u++ + ++u;
    r=u;
    u++;
    ++u;
    r+=u;
    u=r;
    printf("%d\n", u); // 2

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

Ensuite, nous allons à Visual Studio . Visual Studio 2015, vous obtenez:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

    i = 1;
    i = (i++);
    printf("%d\n", i); // 2 

    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 3

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

Comment fonctionne Visual Studio, il adopte une autre approche, il évalue toutes les expressions pré-incrémentées lors de la première passe, puis utilise les valeurs des variables dans les opérations lors de la deuxième passe, attribue de RHS à LHS lors de la troisième passe, puis enfin il évalue toutes les expressions post-incrémentation en une seule passe.

Ainsi, l'équivalent en comportement défini C ++ tel que Visual C ++ le comprend:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

    volatile int u = 0;
    //u = u++ + ++u;
    ++u;
    r = u + u;
    u = r;
    u++;
    printf("%d\n", u); // 3

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

comme l'indique la documentation de Visual Studio sur Priorité et Ordre d'évaluation :

Lorsque plusieurs opérateurs apparaissent ensemble, ils ont la même priorité et sont évalués en fonction de leur associativité. Les opérateurs du tableau sont décrits dans les sections commençant par Opérateurs Postfix.

Mohamed El-Nakib
la source
1
J'ai édité la question pour ajouter l'UB dans l'évaluation des arguments de fonction, car cette question est souvent utilisée en double pour cela. (Le dernier exemple)
Antti Haapala
1
De plus, la question concerne c maintenant, pas C ++
Antti Haapala