Priorité de l’opérateur vs ordre d’évaluation

Les termes «priorité d’opérateur» et «ordre d’évaluation» sont des termes très couramment utilisés dans la programmation et extrêmement importants pour un programmeur. Et, pour autant que je les comprenne, les deux concepts sont étroitement liés; on ne peut pas faire sans l’autre en parlant d’expressions.

Prenons un exemple simple:

int a=1; // Line 1 a = a++ + ++a; // Line 2 printf("%d",a); // Line 3 

Maintenant, il est évident que la Line 2 conduit à un comportement indéfini, puisque les points de séquence en C et C ++ incluent:

  1. Entre l’évaluation des opérandes gauche et droite du && (logique AND), || (OR logique) et les opérateurs de virgule. Par exemple, dans l’expression *p++ != 0 && *q++ != 0 , tous les effets secondaires de la sous-expression *p++ != 0 sont complétés avant toute tentative d’access à q .

  2. Entre l’évaluation du premier opérande de l’opérateur ternaire “point d’interrogation” et du deuxième ou du troisième opérande. Par exemple, dans l’expression a = (*p++) ? (*p++) : 0 a = (*p++) ? (*p++) : 0 il y a un sharepoint séquence après le premier *p++ , ce qui signifie qu’il a déjà été incrémenté au moment où la seconde instance est exécutée.

  3. À la fin d’une expression complète. Cette catégorie inclut des déclarations d’expression (telles que l’affectation a=b; ), des instructions de retour, les expressions de contrôle des instructions if, switch, while ou do-while, et les trois expressions dans une instruction for.

  4. Avant qu’une fonction soit entrée dans un appel de fonction. L’ordre dans lequel les arguments sont évalués n’est pas spécifié, mais ce sharepoint séquence signifie que tous leurs effets secondaires sont complets avant que la fonction ne soit entrée. Dans l’expression f(i++) + g(j++) + h(k++) , f est appelé avec un paramètre de la valeur d’origine de i , mais i est incrémenté avant d’entrer dans le corps de f . De même, j et k sont mis à jour avant d’entrer respectivement g et h . Cependant, il n’est pas spécifié dans quel ordre f() , g() , h() sont exécutés, ni dans quel ordre i , j , k sont incrémentés. Les valeurs de j et k dans le corps de f sont donc indéfinies. 3 Notez qu’un appel de fonction f(a,b,c) n’est pas une utilisation de l’opérateur virgule et que l’ordre d’évaluation pour a , b et c n’est pas spécifié.

  5. Lors d’un retour de fonction, une fois que la valeur de retour est copiée dans le contexte d’appel. (Ce sharepoint séquence n’est spécifié que dans le standard C ++; il n’est présent qu’implicitement dans C.)

  6. À la fin d’un initialiseur; par exemple, après l’évaluation de 5 dans la déclaration int a = 5; .

Ainsi, en passant par le point n ° 3:

À la fin d’une expression complète. Cette catégorie inclut des déclarations d’expression (telles que l’affectation a = b;), des instructions de retour, les expressions de contrôle des instructions if, switch, while ou do-while, et les trois expressions dans une instruction for.

Line 2 conduit clairement à un comportement non défini. Cela montre comment le comportement non défini est étroitement lié aux points de séquence .

Prenons maintenant un autre exemple:

 int x=10,y=1,z=2; // Line 4 int result = x<y<z; // Line 5 

Maintenant, il est évident que Line 5 rendra le magasin de result variables 1 .

Maintenant, l’expression x<y<z dans la Line 5 peut être évaluée soit:

x<(y<z) ou (x<y)<z . Dans le premier cas, la valeur du result sera 0 et dans le second cas, le result sera 1 . Mais nous soaps que lorsque la Operator Precedence l’ Operator Precedence est Equal/Same , l’ Associativity entre en jeu. Elle est donc évaluée en tant que (x<y)<z .

C’est ce qui est dit dans cet article MSDN :

La priorité et l’associativité des opérateurs C affectent le regroupement et l’évaluation des opérandes dans les expressions. La priorité d’un opérateur n’a de sens que si d’autres opérateurs avec une priorité supérieure ou inférieure sont présents. Les expressions avec des opérateurs de priorité supérieure sont évaluées en premier. La préséance peut également être décrite par le mot “binding”. Les opérateurs ayant une priorité plus élevée auraient une liaison plus étroite.

Maintenant, à propos de l’article ci-dessus:

Il mentionne “Les expressions avec des opérateurs de priorité supérieure sont évaluées en premier.”

Cela peut sembler incorrect. Mais je pense que l’article ne dit pas quelque chose de mal si on considère que () est aussi un opérateur x<y<z est identique à (x<y)<z . Mon raisonnement est que si l’associativité n’entre pas en jeu, alors l’évaluation des expressions complètes deviendrait ambiguë puisque < n’est pas un sharepoint séquence .

En outre, un autre lien que j’ai trouvé dit ceci sur la priorité et l’associativité de l’opérateur :

Cette page répertorie les opérateurs C par ordre de priorité (du plus élevé au plus bas). Leur associativité indique dans quel ordre les opérateurs de priorité égale dans une expression sont appliqués.

Donc, en prenant, le deuxième exemple de int result=x<y<z , on peut voir ici qu’il y a dans les 3 expressions, x , y et z , puisque la forme la plus simple d’une expression consiste en une constante littérale ou un object . D’où le résultat des expressions x , y , z serait là des valeurs , c.-à-d. 10 , 1 et 2 respectivement. Par conséquent, nous pouvons maintenant interpréter x<y<z comme 10<1<2 .

Maintenant, l’Associativité n’entre pas en jeu puisque nous avons maintenant 2 expressions à évaluer, soit 10<1 ou 1<2 et comme la priorité de l’opérateur est la même, elles sont évaluées de gauche à droite ?

Prenant ce dernier exemple comme argument:

 int myval = ( printf("Operator\n"), printf("Precedence\n"), printf("vs\n"), printf("Order of Evaluation\n") ); 

Maintenant, dans l’exemple ci-dessus, puisque l’opérateur de comma a la même priorité, les expressions sont évaluées de left-to-right et la valeur de retour du dernier printf() est stockée dans myval .

Dans SO / IEC 9899: 201x sous J.1 Comportement non spécifié, il mentionne:

L’ordre dans lequel les sous-expressions sont évaluées et l’ordre dans lequel les effets secondaires ont lieu, sauf comme spécifié pour les opérateurs function-call (), &&, ||,?: Et comma (6.5).

Maintenant, je voudrais savoir, serait-il faux de dire:

L’ordre d’évaluation dépend de la priorité des opérateurs, laissant des cas de comportement non spécifié.

Je voudrais être corrigé si des erreurs ont été commises dans quelque chose que j’ai dit dans ma question. La raison pour laquelle j’ai posté cette question est à cause de la confusion créée dans mon esprit par l’article MSDN. Est-il en erreur ou non?

    Oui, l’article MSDN est erroné, du moins en ce qui concerne les standards C et C ++ 1 .

    Cela dit, permettez-moi de commencer par une note sur la terminologie: dans le standard C ++, ils utilisent principalement “evaluation” pour désigner un opérande et “calcul de valeur” pour effectuer une opération. Ainsi, lorsque (par exemple) vous faites a + b , chacun des a et b est évalué, puis le calcul de la valeur est effectué pour déterminer le résultat.

    Il est clair que l’ordre des calculs de valeurs est (principalement) contrôlé par la préséance et l’associativité – le calcul des valeurs de contrôle est essentiellement la définition de la préséance et de l’associativité. Le rest de cette réponse utilise “évaluation” pour faire référence à l’évaluation des opérandes et non aux calculs.

    Maintenant, pour que l’ordre d’évaluation soit déterminé par la préséance, non, ce n’est pas le cas! C’est aussi simple que ça. Par exemple, considérons votre exemple de x . Selon les règles d'associativité, cela parsing comme (x . Maintenant, envisagez d’évaluer cette expression sur une machine à stack. Il est parfaitement permis de faire quelque chose comme ceci:

      push(z); // Evaluates its argument and pushes value on stack push(y); push(x); test_less(); // compares TOS to TOS(1), pushes result on stack test_less(); 

    Cela évalue z avant x ou y , mais évalue toujours (x , puis compare le résultat de cette comparaison à z , comme il est supposé le faire.

    Résumé: L'ordre d'évaluation est indépendant de l'associativité.

    La préséance est la même chose. Nous pouvons changer l'expression en x*y+z et évaluer z avant x ou y :

     push(z); push(y); push(x); mul(); add(); 

    Résumé: L'ordre d'évaluation est indépendant de la préséance.

    Si / si nous ajoutons des effets secondaires, cela rest le même. Je pense qu'il est instructif de penser aux effets secondaires comme étant exécutés par un fil d'exécution séparé, avec une join au prochain sharepoint séquence (par exemple, la fin de l'expression). Donc quelque chose comme a=b++ + ++c; pourrait être exécuté quelque chose comme ceci:

     push(a); push(b); push(c+1); side_effects_thread.queue(inc, b); side_effects_thread.queue(inc, c); add(); assign(); join(side_effects_thread); 

    Cela montre également pourquoi une dépendance apparente n'affecte pas nécessairement l'ordre d'évaluation non plus. Même si a est la cible de l'affectation, cela évalue toujours a avant d' évaluer soit b ou c . Notez également que même si je l'ai écrit comme "thread" ci-dessus, cela pourrait aussi bien être un pool de threads, tous s'exécutant en parallèle, de sorte que vous n'obtenez aucune garantie sur l'ordre d'un incrément par rapport à un autre.

    À moins que le matériel ne dispose d'une prise en charge directe (et peu coûteuse ) de la mise en queue sécurisée pour les threads, cela ne serait probablement pas utilisé dans une implémentation réelle (et même alors, cela n'est pas très probable). Placer quelque chose dans une queue sécurisée aura normalement un peu plus de temps qu'un simple incrément, donc il est difficile d'imaginer que quelqu'un le fasse dans la réalité. D'un sharepoint vue conceptuel, cependant, l'idée est conforme aux exigences de la norme: lorsque vous utilisez une opération d'incrémentation / décrémentation pré / post, vous spécifiez une opération qui aura lieu quelque temps après l'évaluation de cette partie de l'expression et sera complète à le prochain sharepoint séquence.

    Edit: bien que ce ne soit pas exactement un threading, certaines architectures permettent une telle exécution parallèle. Pour quelques exemples, les processeurs Intel Itanium et VLIW, tels que certains DSP, permettent à un compilateur de désigner plusieurs instructions à exécuter en parallèle. La plupart des machines VLIW ont une taille d'instruction "paquet" spécifique qui limite le nombre d'instructions exécutées en parallèle. Itanium utilise également des paquets d'instructions, mais désigne un bit dans un paquet d'instructions pour dire que les instructions du paquet en cours peuvent être exécutées en parallèle avec celles du paquet suivant. En utilisant des mécanismes comme celui-ci, vous obtenez des instructions qui s'exécutent en parallèle, comme si vous utilisiez plusieurs threads sur des architectures avec lesquelles la plupart d'entre nous sont plus familiers.

    Résumé: L'ordre d'évaluation est indépendant des dépendances apparentes

    Toute tentative d'utilisation de la valeur avant le prochain sharepoint séquence donne un comportement indéfini - en particulier, "l'autre thread" modifie (potentiellement) ces données pendant ce temps, et vous n'avez aucun moyen de synchroniser l'access avec l'autre thread. Toute tentative d'utilisation conduit à un comportement indéfini.

    Juste pour un exemple (certes, maintenant plutôt tiré par les cheveux), pensez à votre code s'exécutant sur une machine virtuelle 64 bits, mais le véritable matériel est un processeur 8 bits. Lorsque vous incrémentez une variable 64 bits, elle exécute une séquence comme celle-ci:

     load variable[0] increment store variable[0] for (int i=1; i<8; i++) { load variable[i] add_with_carry 0 store variable[i] } 

    Si vous lisez la valeur quelque part au milieu de cette séquence, vous pouvez obtenir quelque chose avec seulement quelques-uns des octets modifiés, donc ce que vous obtenez n'est ni l'ancienne valeur ni la nouvelle.

    Cet exemple exact peut être assez exagéré, mais une version moins extrême (par exemple, une variable 64 bits sur une machine 32 bits) est en réalité assez courante.

    Conclusion

    L'ordre d'évaluation ne dépend pas de la préséance, de l'associativité ou (nécessairement) des dépendances apparentes. Tenter d'utiliser une variable à laquelle un incrément / décrément pré / post a été appliqué dans une autre partie d'une expression donne vraiment un comportement totalement indéfini. Même si un crash réel est improbable, vous n'êtes certainement pas assuré d'obtenir l'ancienne valeur ou la nouvelle - vous pourriez obtenir autre chose entièrement.


    1 Je n'ai pas vérifié cet article en particulier, mais plusieurs articles MSDN parlent de Microsoft C ++ et / ou C ++ / CLI gérés par Microsoft, mais ne font rien ou presque rien pour signaler qu'ils ne s'appliquent pas à C ou C ++ standard. Cela peut donner l'impression fausse qu'ils prétendent que les règles qu'ils ont décidé d'appliquer à leurs propres langues s'appliquent réellement aux langues standard. Dans ces cas, les articles ne sont pas techniquement faux - ils n'ont tout simplement rien à voir avec les standards C ou C ++. Si vous essayez d'appliquer ces instructions à la norme C ou C ++, le résultat est faux.

    La seule façon dont la préséance influence l’ordre d’évaluation est qu’elle crée des dépendances; sinon les deux sont orthogonaux. Vous avez soigneusement choisi des exemples sortingviaux où les dépendances créées par la priorité finissent par définir complètement l’ordre d’évaluation, mais ce n’est généralement pas vrai. Et n’oubliez pas non plus que de nombreuses expressions ont deux effets: elles donnent une valeur et ont des effets secondaires. Ces deux ne sont pas obligés de se produire ensemble, donc même lorsque les dépendances forcent un ordre spécifique d’évaluation, il ne s’agit que de l’ordre d’évaluation des valeurs; il n’a aucun effet sur les effets secondaires.

    Une bonne façon de voir cela est de prendre l’arbre d’expression.

    Si vous avez une expression, disons x+y*z vous pouvez réécrire cela dans un arbre d’expression:

    Appliquer les règles de priorité et d’associativité:

     x + ( y * z ) 

    Après avoir appliqué les règles de priorité et d’associativité, vous pouvez les oublier en toute sécurité.

    En forme d’arbre:

      x + y * z 

    Maintenant, les feuilles de cette expression sont x , y et z . Cela signifie que vous pouvez évaluer x , y et z dans l’ordre de votre choix. Cela signifie également que vous pouvez évaluer le résultat de * et x dans n’importe quel ordre.

    Maintenant que ces expressions n’ont pas d’effets secondaires, vous ne vous en souciez pas vraiment. Mais s’ils le font, la commande peut changer le résultat, et puisque le compilateur peut décider de n’importe quoi, vous avez un problème.

    Maintenant, les points de séquence apportent un peu d’ordre à ce chaos. Ils coupent efficacement l’arbre en sections.

    x + y * z, z = 10, x + y * z

    après priorité et associativité

    x + ( y * z ) , z = 10, x + ( y * z)

    l’arbre:

      x + y * z , ------------ z = 10 , ------------ x + y * z 

    La partie supérieure de l’arbre sera évaluée avant le milieu et le milieu avant le bas.

    Il mentionne “Les expressions avec des opérateurs de priorité supérieure sont évaluées en premier.”

    Je vais juste répéter ce que j’ai dit ici . En ce qui concerne les standards C et C ++, cet article est défectueux. La priorité affecte uniquement les jetons considérés comme étant les opérandes de chaque opérateur, mais n’affecte en rien l’ordre d’évaluation.

    Ainsi, le lien explique uniquement comment Microsoft a implémenté les choses, et non comment le langage lui-même fonctionne.

    La préséance n’a rien à voir avec l’ordre d’évaluation et vice-versa.

    Les règles de priorité décrivent comment une expression sous-paramétrée doit être mise entre parenthèses lorsque l’expression mélange différents types d’opérateurs. Par exemple, la multiplication est prioritaire par rapport à l’addition, donc 2 + 3 x 4 est équivalent à 2 + (3 x 4) , pas (2 + 3) x 4 .

    L’ordre des règles d’ évaluation décrit l’ordre dans lequel chaque opérande d’une expression est évaluée.

    Prenons un exemple

     y = ++x || --y; 

    Par règle de priorité d’opérateur, il sera mis entre parenthèses sous la forme ( ++/-- a une priorité supérieure à || qui a une priorité supérieure à = ):

     y = ( (++x) || (--y) ) 

    L’ordre d’évaluation de OU logique || indique que (C11 6.5.14)

    le || l’opérateur garantit une évaluation de gauche à droite .

    Cela signifie que l’opérande gauche, c’est-à-dire la sous-expression (x++) sera évaluée en premier. En raison d’un comportement de court-circuit; Si le premier opérande est différent de 0 , le deuxième opérande n’est pas évalué , l’opérande droite --y ne sera pas évalué bien qu’il soit entre parenthèses avant (++x) || (--y) (++x) || (--y) .

    Je pense que c’est seulement le

     a++ + ++a 

    problème d’épxression, car

     a = a++ + ++a; 

    correspond d’abord à 3. mais ensuite à la règle 6.: complète l’évaluation avant l’affectation.

    Alors,

     a++ + ++a 

    obtient pour un = 1 entièrement évalué à:

     1 + 3 // left to right, or 2 + 2 // right to left 

    Le résultat est identique = 4.

    Un

     a++ * ++a // or a++ == ++a 

    aurait des résultats indéfinis. N’est-ce pas?