Pourquoi l'opérateur ternaire avec des virgules n'évalue-t-il qu'une seule expression dans le cas vrai?

119

J'apprends actuellement le C ++ avec le livre C ++ Primer et l'un des exercices du livre est:

Expliquez ce que fait l'expression suivante: someValue ? ++x, ++y : --x, --y

Que savons-nous? Nous savons que l'opérateur ternaire a une priorité plus élevée que l'opérateur virgule. Avec les opérateurs binaires, c'était assez facile à comprendre, mais avec l'opérateur ternaire, je me bats un peu. Avec les opérateurs binaires "ayant une priorité plus élevée" signifie que nous pouvons utiliser des parenthèses autour de l'expression avec une priorité plus élevée et cela ne changera pas l'exécution.

Pour l'opérateur ternaire, je ferais:

(someValue ? ++x, ++y : --x, --y)

résultant effectivement dans le même code qui ne m'aide pas à comprendre comment le compilateur regroupera le code.

Cependant, après avoir testé avec un compilateur C ++, je sais que l'expression se compile et je ne sais pas ce qu'un :opérateur pourrait représenter par lui-même. Le compilateur semble donc interpréter correctement l'opérateur ternaire.

Ensuite, j'ai exécuté le programme de deux manières:

#include <iostream>

int main()
{
    bool someValue = true;
    int x = 10, y = 10;

    someValue ? ++x, ++y : --x, --y;

    std::cout << x << " " << y << std::endl;
    return 0;
}

Résulte en:

11 10

Alors que d'autre part avec someValue = falseelle imprime:

9 9

Pourquoi le compilateur C ++ générerait-il du code qui pour la vraie branche de l'opérateur ternaire n'incrémente que x, tandis que pour la fausse branche du ternaire, il décrémente à la fois xet y?

Je suis même allé jusqu'à mettre des parenthèses autour de la vraie branche comme ceci:

someValue ? (++x, ++y) : --x, --y;

mais il en résulte toujours 11 10.

Aufziehvogel
la source
5
La "préséance" n'est qu'un phénomène émergent en C ++. Il peut être plus simple de regarder directement la grammaire du langage et de voir comment les expressions fonctionnent.
Kerrek SB
26
Nous ne nous soucions pas que beaucoup sur les principes. :-) Le fait que vous deviez demander ceci ici indique que le code ne passera jamais une révision de code par vos collègues programmeurs. Cela rend la connaissance de la façon dont cela fonctionne réellement moins qu'utile. À moins que vous ne souhaitiez participer au concours Obfuscated C , bien sûr.
Bo Persson
5
@BoPersson sans exemples comme celui-ci pour apprendre, les futurs critiques ne sauront jamais pourquoi ils devraient rejeter cela de la production.
Leushenko
8
@Leushenko - Les cloches d'avertissement devraient sonner de toute façon. Plusieurs incréments et décrémentations dans la même instruction (ding, ding, ding!). Un opérateur ternaire où vous pourriez utiliser if-else (ding, ding, ding!). Attendez, ces virgules sont-elles l'opérateur de virgule redouté? (Ding, DING, DING!) Avec tous ces opérateurs, pourrait-il y avoir quelque chose de précédent? (ding, ding, ding!) Donc, nous ne pourrons jamais l'utiliser. Alors pourquoi perdre du temps à comprendre ce qu'il fait, le cas échéant?
Bo Persson
4
Minor nit: le nom de ?est l' opérateur conditionnel . Le terme opérateur ternaire signifie simplement un opérateur avec trois opérandes. L'opérateur conditionnel est un exemple d' opérateur ternaire, mais un langage pourrait (théoriquement) avoir plusieurs opérateurs ternaires.
bta

Réponses:

122

Comme l'a dit @Rakete dans leur excellente réponse, c'est délicat. J'aimerais ajouter quelque chose à cela.

L'opérateur ternaire doit avoir la forme:

? expression d' affectation d' expression logique ou d' : expression

Nous avons donc les mappages suivants:

  • someValue: expression-logique
  • ++x, ++y: expression
  • ??? est une expression d'affectation --x, --y ou seulement --x?

En fait, c'est uniquement --xparce qu'une expression d'affectation ne peut pas être analysée comme deux expressions séparées par une virgule (selon les règles de grammaire de C ++) et --x, --yne peut donc pas être traitée comme une expression d'affectation .

Ce qui donne à la partie d'expression ternaire (conditionnelle) l'aspect suivant:

someValue?++x,++y:--x

Pour des raisons de lisibilité, il peut être utile de considérer ++x,++yqu'il est calculé comme s'il était entre parenthèses (++x,++y); tout ce qui est contenu entre ?et :sera séquencé après le conditionnel. (Je vais les mettre entre parenthèses pour le reste de l'article).

et évalué dans cet ordre:

  1. someValue?
  2. (++x,++y)ou --x(selon le boolrésultat de 1.)

Cette expression est ensuite traitée comme la sous-expression gauche d'un opérateur virgule, la sous-expression droite étant --y, comme ceci:

(someValue?(++x,++y):--x), --y;

Ce qui signifie que le côté gauche est une expression à valeur rejetée , ce qui signifie qu'il est définitivement évalué, mais nous évaluons ensuite le côté droit et le retournons.

Alors qu'est - ce qui se passe quand someValueest true?

  1. (someValue?(++x,++y):--x)exécute et incrémente xet yêtre 11et11
  2. L'expression de gauche est ignorée (bien que les effets secondaires de l'incrémentation demeurent)
  3. Nous évaluons le côté droit de l'opérateur virgule:, --yqui décrémente yensuite10

Pour "corriger" le comportement, vous pouvez le regrouper --x, --yavec des parenthèses pour le transformer en une expression principale qui est une entrée valide pour une expression d'affectation *:

someValue?++x,++y:(--x, --y);

* C'est une longue chaîne plutôt amusante qui relie une expression d'affectation à une expression principale:

cession d'expression --- (peut consister) -> conditionnel expression -> ou expression logique -> et l' expression logique -> inclusive ou expression -> ou expression exclusive - -> et-expression -> égalité-expression -> relation-expression -> shift-expression -> additive-expression -> multiplicative-expression -> pm-expression -> cast-expression -> expression-unaire -> expression-suffixe -> expression-primaire

AndyG
la source
10
Merci d'avoir pris la peine de démêler les règles de grammaire; cela montre que la grammaire C ++ contient plus que ce que vous trouverez dans la plupart des manuels.
sdenham
4
@sdenham: Quand les gens demandent pourquoi les "langages orientés expression" sont gentils (c'est-à-dire quand { ... }peuvent être traités comme une expression), j'ai maintenant une réponse => c'est pour éviter d'avoir à introduire un opérateur virgule qui se comporte de manière si délicate.
Matthieu M.
Pouvez-vous me donner un lien pour en savoir plus sur la assignment-expressionchaîne?
MiP
@MiP: Je l'ai retiré du standard lui-même, vous pouvez le trouver sous gram.expr
AndyG
88

Wow, c'est délicat.

Le compilateur voit votre expression comme:

(someValue ? (++x, ++y) : --x), --y;

L'opérateur ternaire a besoin de a :, il ne peut pas se tenir seul dans ce contexte, mais après cela, il n'y a aucune raison pour que la virgule appartienne au cas faux.

La raison pour laquelle vous obtenez cette sortie peut maintenant être plus logique. Si someValueest vrai, alors ++x, ++yet --yest exécuté, ce qui ne change pas efficacement ymais en ajoute un à x.

Si someValueest faux, alors --xet --ysont exécutés, en les décrémentant tous les deux de un.

Rakete1111
la source
42

Pourquoi le compilateur C ++ générerait-il du code qui pour la vraie branche de l'opérateur ternaire n'incrémente que x

Vous avez mal interprété ce qui s'est passé. La vraie branche incrémente à la fois xet y. Cependant, yest décrémenté immédiatement après cela, sans condition.

Voici comment cela se produit: puisque l'opérateur conditionnel a une priorité plus élevée que l'opérateur virgule en C ++ , le compilateur analyse l'expression comme suit:

   (someValue ? ++x, ++y : --x), (--y);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^

Notez le "orphelin" --yaprès la virgule. C'est ce qui conduit à une décrémentation yqui a été initialement incrémentée.

Je suis même allé jusqu'à mettre des parenthèses autour de la vraie branche comme ceci:

someValue ? (++x, ++y) : --x, --y;

Vous étiez sur le bon chemin, mais vous avez mis entre parenthèses une mauvaise branche: vous pouvez résoudre ce problème en mettant entre parenthèses la branche else, comme ceci:

someValue ? ++x, ++y : (--x, --y);

Démo (imprime 11 11)

dasblinkenlight
la source
5

Votre problème est que l'expression ternaire n'a pas vraiment de priorité plus élevée que la virgule. En fait, C ++ ne peut pas être décrit avec précision simplement par priorité - et c'est exactement l'interaction entre l'opérateur ternaire et la virgule où il se décompose.

a ? b++, c++ : d++

est traité comme:

a ? (b++, c++) : d++

(la virgule se comporte comme si elle avait une priorité plus élevée). D'autre part,

a ? b++ : c++, d++

est traité comme:

(a ? b++ : c++), d++

et l'opérateur ternaire a une priorité plus élevée.

Martin Bonner soutient Monica
la source
Je pense que cela est toujours dans le domaine de la priorité puisqu'il n'y a qu'une seule analyse valide pour la ligne médiane, non? Encore un exemple utile
sudo rm -rf slash
2

Un point qui a été négligé dans les réponses (bien qu'effleuré dans les commentaires) est que l'opérateur conditionnel est invariablement utilisé (prévu par la conception?) Dans le code réel comme raccourci pour attribuer l'une des deux valeurs à une variable.

Ainsi, le contexte plus large serait:

whatIreallyWanted = someValue ? ++x, ++y : --x, --y;

Ce qui est absurde à première vue, les crimes sont donc multiples:

  • Le langage permet des effets secondaires ridicules dans une mission.
  • Le compilateur ne vous a pas averti que vous faisiez des choses bizarres.
  • Le livre semble se concentrer sur des questions «pièges». On ne peut qu'espérer que la réponse au verso était "Ce que cette expression fait dépend de cas étranges dans un exemple artificiel pour produire des effets secondaires auxquels personne ne s'attend. Ne faites jamais cela."
Taryn
la source
1
L'affectation d'une des deux variables est le cas habituel de l'opérateur ternaire, mais il y a des occasions où il est utile d'avoir une forme d'expression deif (par exemple, l'expression d'incrémentation dans une boucle for). Le contexte plus large pourrait bien être for (x = 0, y=0; x+y < 100; someValue?(++x, ++y) :( --x, --y))avec une boucle qui peut modifier xet yindépendamment.
Martin Bonner soutient Monica
@MartinBonner Je ne suis pas convaincu, et cet exemple semble assez bien faire valoir le point de vue de bo-perrson, comme il a cité Tony Hoare.
Taryn