Laquelle des combinaisons suivantes d’opérateurs de post-pré-incrémentation a un comportement indéfini en C?

J’ai lu, Quelqu’un pourrait-il expliquer ces comportements non définis (i = i ++ + ++ i, i = i ++, etc …) et essayé de comprendre les points de séquence sur “comp.lang.c FAQ” après avoir perdu plus de 2 heures temps en essayant d’expliquer les résultats suivants par le compilateur gcc.

expression(i=1;j=2) ijk k = i++ + j++; 2 3 3 k = i++ + ++j; 2 3 4 k = ++i + j++; 2 3 4 k = ++i + ++j; 2 3 5 k = i++ + i++; 3 2 k = i++ + ++i; 3 4 k = ++i + i++; 3 4 k = ++i + ++i; 3 6 i = i++ + j++; 4 3 i = i++ + ++j; 5 3 i = ++i + j++; 4 3 i = ++i + ++j; 5 3 i = i++ + i++; 4 i = i++ + ++i; 5 i = ++i + i++; 5 i = ++i + ++i; 6 

Question:

  1. Je veux savoir si toutes les expressions présentées (dans 4 groupes) dans la figure ci-dessus ont un comportement indéfini? Si seulement certains d’entre eux ont un comportement indéfini, lesquels ont-ils et lesquels pas?

  2. Pour les expressions à comportement défini, veuillez indiquer (et non expliquer) comment le compilateur les évalue. Juste pour être sûr, si j’ai ce pré-incrément et post-incrément correctement.

Contexte:

Aujourd’hui, j’ai assisté à une interview sur le campus dans laquelle on m’a demandé d’expliquer les résultats de i++ + ++i pour une valeur donnée de i . Après avoir compilé cette expression dans gcc, je me suis rendu compte que la réponse que j’avais donnée en interview était fausse. J’ai décidé de ne pas commettre une telle erreur à l’avenir et par conséquent, j’ai essayé de comstackr toutes les combinaisons possibles d’opérateurs de pré-incrémentation et de post-incrémentation et de les comstackr dans gcc, puis d’essayer d’expliquer les résultats. J’ai lutté pendant plus de 2 heures. Je n’ai pas trouvé de comportement unique d’évaluation de ces expressions. Donc, j’ai abandonné et me suis tourné vers stackoverflow. Après un peu de lecture des archives, il s’est avéré qu’il y avait quelque chose comme un sequence point et un comportement indéfini.

À l’exception du premier groupe, toutes les expressions des trois autres groupes ont un comportement indéfini.

Comment le comportement défini est évalué (groupe 1):

 i=1, j=2; k=i++ + j++; // 1 + 2 = 3 k=i++ + ++j; // 1 + 3 = 4 k=++i + ++j; // 2 + 3 = 5 k=++i + j++; // 2 + 2 = 4 

C’est assez simple. post-incrémentation vs pré-incrémentation.

Dans les groupes 2 et 4, il est assez facile de voir les comportements non définis.

Le groupe 2 a un comportement indéfini car l’opérateur = n’introduit pas de sharepoint séquence.

Il n’y a pas de points de séquence dans aucune de ces déclarations. Il y a des points de séquence entre eux.

Si vous modifiez le même object deux fois entre des points de séquence consécutifs (dans ce cas, via = via préfixe ou postfix ++ ), le comportement est indéfini. Le comportement du premier groupe de 4 déclarations est donc bien défini. le comportement des autres est indéfini.

Si le comportement est défini, i++ renvoie la valeur précédente de i et modifie i en lui ajoutant 1 . ++i modifie i en lui ajoutant 1 , puis renvoie la valeur modifiée.

Je veux savoir si toutes les expressions présentées (dans 4 groupes) dans la figure ci-dessus ont un comportement indéfini?

Lignes 2 à 5:

 k = i++ + j++; k = i++ + ++j; k = ++i + ++j; k = ++i + j++; 

sont tous bien définis. Toutes les autres expressions sont indéfinies, car elles tentent toutes de modifier la valeur d’un object en évaluant une expression plusieurs fois entre les points de séquence (pour ces exemples, le sharepoint séquence apparaît à la fin de chaque instruction). Par exemple, i = i++; est indéfini car nous essayons de modifier la valeur de i via une affectation et un postfix ++ sans sharepoint séquence intermédiaire. FYI = opérateur n’introduit pas de sharepoint séquence. || && ?: et ,comma opérateurs de ,comma introduisent des points de séquence

Pour les expressions à comportement défini, veuillez indiquer (et non expliquer) comment le compilateur les évalue.

Commençons avec

 k = i++ + j++; 

L’expression a++ à la valeur actuelle de a et, avant le prochain sharepoint séquence, a est incrémentée de 1. Donc, logiquement , l’évaluation ressemble à quelque chose comme:

 k = 1 + 2; // i++ evaluates to 1, j++ evaluates to 2 i = i + 1; // i is incremented and becomes 2 j = j + 1; // j is incremented and becomes 3 

Toutefois…

L’ordre exact dans lequel les expressions i++ et j++ sont évaluées, ainsi que l’ordre dans lequel leurs effets secondaires sont appliqués, n’est pas spécifié . Voici un ordre des opérations parfaitement raisonnable (utilisant un pseudo-code d’assemblage):

 mov j, r0 ; read the value of j into register r0 mov i, r1 ; read the value of i into register r1 add r0, r1, r2 ; add the contents of r0 to r1, store result to r2 mov r2, k ; write result to k inc r1 ; increment value of i inc r0 ; increment value of j mov r0, j ; store result of j++ mov r1, i ; store result of i++ 

NE PAS ASSUMER UNE ÉVALUATION DES EXPRESSIONS ARITHMÉTIQUES GAUCHE À DROITE. N’ASSUMEZ PAS QUE LES OPÉRANDES DE ++ et -- SONT MIS À JOUR IMMÉDIATEMENT APRÈS L’ÉVALUATION.

Pour cette raison, le résultat d’expressions telles que i++ + ++i varie en fonction du compilateur, de ses parameters et même du code qui l’entoure. Le comportement est laissé indéfini afin que le compilateur ne soit pas obligé de “faire la bonne chose”, peu importe ce que cela peut être. Vous obtiendrez un résultat, mais ce ne sera pas nécessairement le résultat que vous espériez et il ne sera pas cohérent sur toutes les plateformes.

Regarder

 k = i++ + ++j; 

l’évaluation logique est

 k = 1 + 3 // i++ evaluates to i (1), ++j evaluates to j + 1 (2 + 1 = 3) i = i + 1 j = j + 1 

Encore une fois, voici une commande possible des opérations:

 mov j, r0 inc r0 mov i, r1 add r0, r1, r2 mov r2, k mov r0, j inc r1 mov r1, i 

Ou cela pourrait faire autre chose. Le compilateur est libre de changer l’ordre dans lequel les expressions individuelles sont évaluées si cela conduit à un ordre d’opérations plus efficace (ce que mes exemples ne le sont certainement pas).

Le premier groupe sont tous définis. Ils incrémentent tous les valeurs de i et j comme effet secondaire quelque temps avant le sharepoint séquence suivant, de sorte que i est laissé égal à 2 et j à 3. Par ailleurs, i++ égal à 1, ++i égal à 2, j++ évalue to 2 et ++j évalué à 3. Cela signifie que le premier atsortingbue 1 + 2 à k , le second atsortingbue 1 + 3 à k , le troisième atsortingbue 2 + 3 à k et le quasortingème atsortingbue 2 + 2 à k .

Les autres sont tous des comportements indéfinis. Dans les deuxième et troisième groupes, i est modifié deux fois avant un sharepoint séquence; dans le quasortingème groupe, i est modifié trois fois avant un sharepoint séquence.

Dans les cas où un compilateur peut dire que deux expressions lvalue identifient le même object, le comportement de celui-ci ne présente aucun coût significatif. Les scénarios les plus intéressants sont ceux dans lesquels un ou plusieurs opérandes sont des pointeurs déréférencés.

Compte tenu du code:

 void test(unsigned *a, unsigned *b, unsigned *c) { (*a) = (*b)++ + (*c)++; } 

un compilateur peut traiter cela de nombreuses manières sensées. Il pourrait charger b et c, les append, stocker le résultat dans a, puis incrémenter b et c, ou alors charger a et b, calculer a + b, a + 1 et b + 1, puis les écrire dans séquence arbitraire, ou effectuer l’une des innombrables autres séquences d’opérations. Sur certains processeurs, certains arrangements pourraient être plus efficaces que d’autres, et un compilateur ne devrait avoir aucune raison de s’attendre à ce que les programmeurs considèrent un arrangement comme plus approprié que tout autre.

Notez que, même si sur la plupart des plates-formes matérielles, il pourrait y avoir un nombre limité de comportements plausibles pouvant découler du partage d’indicateurs identiques en a, b et c, les auteurs de la norme ne font aucun effort pour distinguer les résultats plausibles des invraisemblables. Même si de nombreuses implémentations pourraient facilement, à un coût pratiquement nul, offrir une certaine garantie comportementale (par exemple, garantir que le code ci-dessus définit toujours *a , *b et *c sur des valeurs éventuellement non spécifiées sans autre effet secondaire), et même bien qu’une telle garantie puisse parfois être utile (si les pointeurs identifient des objects distincts dans les cas où les valeurs des objects importent, mais ne le font pas autrement), il est à la mode pour les rédacteurs de compilateur de considérer toute possibilité légère d’optimisation qu’ils pourraient réaliser lorsqu’ils seraient accordés. carte blanche pour déclencher des effets secondaires arbitrairement destructeurs vaudra plus que la valeur que les programmeurs pourraient recevoir d’une assurance de comportement limité.

Dans la plupart des cas, gcc implémente d’abord les pré-incrémentations et utilise ces valeurs dans les opérations, puis évalue les post-incréments.

Par exemple. Dans le bloc 2, les pré-incréments sont nuls, donc i 1 est utilisé

 k = i++ + i++ // hence k = 1+1=2 

Et deux incréments de poste dans i si i = 3

Un pré-incrément change i en 2

 k = i++ + ++i // hence k= 2+2= 4 

Un incrément de poste dans i donc i= 3

Idem pour k= ++i + i++

Deux pré-incréments dans i rend 3

 k=++i + ++i // hence k=3+3= 6 

Et i = 3

J’espère que ça explique un peu. Mais cela dépend purement du compilateur.