Les opérateurs logiques de court-circuit sont-ils obligatoires? Et ordre d'évaluation?

140

La norme ANSI mandat à court-circuiter les opérateurs logiques, en C ou C ++?

Je suis confus car je me souviens du livre K&R disant que votre code ne devrait pas dépendre du court-circuit de ces opérations, car elles peuvent ne pas l'être. Quelqu'un pourrait-il indiquer où dans la norme il est dit que les opérations logiques sont toujours court-circuitées? Je suis surtout intéressé par C ++, une réponse également pour C serait géniale.

Je me souviens aussi avoir lu (je ne me souviens pas où) que l'ordre d'évaluation n'est pas strictement défini, donc votre code ne devrait pas dépendre ou assumer que les fonctions d'une expression seraient exécutées dans un ordre spécifique: à la fin d'une instruction, toutes les fonctions référencées aura été appelé, mais le compilateur a la liberté de sélectionner l'ordre le plus efficace.

La norme indique-t-elle l'ordre d'évaluation de cette expression?

if( functionA() && functionB() && functionC() ) cout<<"Hello world";
Joe Pineda
la source
12
Attention: c'est vrai pour les types POD. Mais si vous surchargez l'opérateur && ou l'opérateur || pour une classe particulière ce ne sont PAS je répète PAS de raccourci. C'est pourquoi il est conseillé de NE PAS définir ces opérateurs pour vos propres classes.
Martin York
J'ai redéfini ces opérateurs il y a quelque temps, lorsque j'ai créé une classe qui ferait des opérations basiques d'algèbre booléenne. Devrait probablement coller un commentaire d'avertissement "cela détruit les courts-circuits et l'évaluation gauche-droite!" au cas où j'oublierais ça. Également surchargé * / + et en a fait leurs synonymes :-)
Joe Pineda
Avoir des appels de fonction dans un bloc if n'est pas une bonne pratique de programmation. Ayez toujours une variable déclarée qui contient la valeur de retour de la méthode et utilisez-la dans le bloc if.
SR Chaitanya
6
@SRChaitanya Ce n'est pas correct. Ce que vous décrivez arbitrairement comme une mauvaise pratique est fait tout le temps, en particulier avec les fonctions qui renvoient des booléens, comme ici.
Marquis of Lorne le

Réponses:

154

Oui, un court-circuit et un ordre d'évaluation sont requis pour les opérateurs ||et &&dans les normes C et C ++.

La norme C ++ dit (il devrait y avoir une clause équivalente dans la norme C):

1.9.18

Dans l'évaluation des expressions suivantes

a && b
a || b
a ? b : c
a , b

en utilisant la signification intégrée des opérateurs dans ces expressions, il y a un point de séquence après l'évaluation de la première expression (12).

En C ++, il y a un piège supplémentaire: le court-circuit ne s'applique PAS aux types qui surchargent les opérateurs ||et &&.

Note de bas de page 12: Les opérateurs indiqués dans ce paragraphe sont les opérateurs intégrés, comme décrit dans l'article 5. Lorsqu'un de ces opérateurs est surchargé (article 13) dans un contexte valide, désignant ainsi une fonction d'opérateur définie par l'utilisateur, l'expression désigne une invocation de fonction, et les opérandes forment une liste d'arguments, sans point de séquence implicite entre eux.

Il n'est généralement pas recommandé de surcharger ces opérateurs en C ++, sauf si vous avez une exigence très spécifique. Vous pouvez le faire, mais cela peut interrompre le comportement attendu dans le code d'autres personnes, en particulier si ces opérateurs sont utilisés indirectement via des modèles d'instanciation avec le type surchargeant ces opérateurs.

Alex B
la source
3
Je ne savais pas que le court-circuit ne s'appliquerait pas aux opérations logiques surchargées, c'est intéressant. Pouvez-vous s'il vous plaît ajouter une référence à la norme ou à une source? Je ne me méfie pas de vous, je veux juste en savoir plus à ce sujet.
Joe Pineda
4
ouais, c'est logique. il agit comme des arguments pour l'opérateur && (a, b). c'est sa mise en œuvre qui dit ce qui se passe.
Johannes Schaub - litb
10
litb: Il n'est tout simplement pas possible de passer b à l'opérateur && (a, b) sans l'évaluer. Et il n'y a aucun moyen d'annuler l'évaluation de b car le compilateur ne peut pas garantir qu'il n'y a pas d'effets secondaires.
jmucchiello
2
Je trouve cela triste. J'aurais pensé ça si j'avais redéfini les opérateurs && et || et ils sont encore totalement déterministes , le compilateur le détecterait et garderait leur évaluation court-circuitée: après tout, l'ordre n'est pas pertinent, et ils ne garantissent aucun effet secondaire!
Joe Pineda
2
@Joe: mais la valeur de retour et les arguments de l'opérateur peuvent changer de booléen à autre chose. J'avais l'habitude d'implémenter une logique "spéciale" avec TROIS valeurs ("vrai", "faux" et "inconnu"). La valeur de retour est déterministe, mais le comportement de court-circuit n'est pas approprié.
Alex B
70

L'évaluation des courts-circuits et l'ordre d'évaluation sont une norme sémantique obligatoire en C et C ++.

Si ce n'était pas le cas, un code comme celui-ci ne serait pas un idiome courant

   char* pChar = 0;
   // some actions which may or may not set pChar to something
   if ((pChar != 0) && (*pChar != '\0')) {
      // do something useful

   }

La section 6.5.13 Opérateur ET logique de la spécification C99 (lien PDF) dit

(4). Contrairement à l'opérateur binaire & au niveau du bit, l'opérateur && garantit une évaluation de gauche à droite; il y a un point de séquence après l'évaluation du premier opérande. Si le premier opérande est égal à 0, le deuxième opérande n'est pas évalué.

De même, la section 6.5.14 Opérateur logique OR dit

(4) Contrairement au bitwise | opérateur, le || l'opérateur garantit une évaluation de gauche à droite; il y a un point de séquence après l'évaluation du premier opérande. Si le premier opérande n'est pas égal à 0, le deuxième opérande n'est pas évalué.

Des termes similaires peuvent être trouvés dans les normes C ++, consultez la section 5.14 de ce projet de copie . Comme les vérificateurs le notent dans une autre réponse, si vous remplacez && ou ||, les deux opérandes doivent être évalués car ils deviennent un appel de fonction normal.

Paul Dixon
la source
Ah, ce que je cherchais! OK, donc l'ordre d'évaluation et le court-circuit sont obligatoires conformément à la norme ANSI-C 99! J'adorerais vraiment voir la référence équivalente pour ANSI-C ++, même si je suis presque à 99%, ce doit être la même chose.
Joe Pineda
Difficile de trouver un bon lien gratuit pour les normes C ++, j'ai un lien vers un projet de copie que j'ai trouvé avec quelques recherches sur Google.
Paul Dixon
Vrai pour les types POD. Mais si vous surchargez l'opérateur && ou l'opérateur || ce ne sont pas des raccourcis.
Martin York
1
oui, il est intéressant de noter que pour bool, vous aurez toujours un ordre d'évaluation garanti et un comportement de court-circuit. car vous ne pouvez pas surcharger l'opérateur && pour deux types intégrés. vous avez besoin d'au moins un type défini par l'utilisateur dans les opérandes pour qu'il se comporte différemment.
Johannes Schaub - litb
J'aimerais pouvoir accepter les deux Checkers et cette réponse. Comme je suis surtout intéressé par C ++, j'accepte l'autre, mais je dois admettre que c'est superbe aussi! Merci beaucoup!
Joe Pineda
19

Oui, cela l'exige (ordre d'évaluation et court-circuit). Dans votre exemple, si toutes les fonctions retournent true, l'ordre des appels est strictement à partir de functionA puis functionB puis functionC. Utilisé pour cela comme

if(ptr && ptr->value) { 
    ...
}

Idem pour l'opérateur virgule:

// calls a, then b and evaluates to the value returned by b
// which is used to initialize c
int c = (a(), b()); 

On dit entre la gauche et la droite opérande &&, ||, ,et entre le premier et le deuxième / troisième opérande ?:(opérateur conditionnel) est un « point de séquence ». Tous les effets secondaires sont évalués complètement avant ce point. Donc, c'est sûr:

int a = 0;
int b = (a++, a); // b initialized with 1, and a is 1

Notez que l'opérateur virgule ne doit pas être confondu avec la virgule syntaxique utilisée pour séparer les choses:

// order of calls to a and b is unspecified!
function(a(), b());

La norme C ++ dit dans 5.14/1:

L'opérateur && regroupe de gauche à droite. Les opérandes sont tous deux implicitement convertis en type bool (clause 4). Le résultat est vrai si les deux opérandes sont vrai et faux dans le cas contraire. Contrairement à &, && garantit une évaluation de gauche à droite: le deuxième opérande n'est pas évalué si le premier opérande est faux.

Et dans 5.15/1:

Le || groupes d'opérateurs de gauche à droite. Les opérandes sont tous deux implicitement convertis en booléen (clause 4). Il renvoie vrai si l'un de ses opérandes est vrai et faux dans le cas contraire. Contrairement à |, || garantit une évaluation de gauche à droite; de plus, le deuxième opérande n'est pas évalué si le premier opérande est évalué à vrai.

Il dit pour les deux à côté de ceux-ci:

Le résultat est un booléen. Tous les effets secondaires de la première expression, à l'exception de la destruction des temporaires (12.2), se produisent avant que la seconde expression ne soit évaluée.

En plus de cela, 1.9/18dit

Dans l'évaluation de chacune des expressions

  • a && b
  • a || b
  • a ? b : C
  • a , b

en utilisant la signification intégrée des opérateurs dans ces expressions (5.14, 5.15, 5.16, 5.18), il y a un point de séquence après l'évaluation de la première expression.

Johannes Schaub - litb
la source
10

Tout droit sorti du bon vieux K&R:

C garantit cela &&et ||sont évalués de gauche à droite - nous verrons bientôt des cas où cela compte.

John T
la source
3
K&R 2e édition p40. "Les expressions connectées par && ou || sont évaluées de gauche à droite, et l'évaluation s'arrête dès que la vérité ou la fausseté du résultat est connue. La plupart des programmes C s'appuient sur ces propriétés." Je ne trouve votre texte cité nulle part dans le livre. Est-ce de la 1ère édition extrêmement obsolète? Veuillez préciser où vous avez trouvé ce texte.
Lundin
1
Ok, il s'avère que vous citez cet ancien tutoriel . Il date de 1974 et n'a aucune importance.
Lundin
6

Soyez très très prudent.

Pour les types fondamentaux, il s'agit d'opérateurs de raccourcis.

Mais si vous définissez ces opérateurs pour vos propres types de classe ou d'énumération, ils ne sont pas des raccourcis. En raison de cette différence sémantique dans leur utilisation dans ces différentes circonstances, il est recommandé de ne pas définir ces opérateurs.

Pour les types fondamentaux operator &&et operator ||pour les types fondamentaux, l'ordre d'évaluation est de gauche à droite (sinon un raccourci serait difficile :-) Mais pour les opérateurs surchargés que vous définissez, il s'agit essentiellement de sucre syntaxique pour définir une méthode et donc l'ordre d'évaluation des paramètres est indéfini.

Martin York
la source
1
La surcharge de l'opérateur n'a rien à voir avec le type POD ou non. Pour définir une fonction d'opérateur, au moins l'un des arguments doit être une classe (ou une structure ou une union) ou une énumération, ou une référence à l'un d'entre eux. Être POD signifie que vous pouvez utiliser memcpy dessus.
Derek Ledbetter
Et c'est ce que je disais. Si vous surchargez && pour votre classe, il ne s'agit en réalité que d'un appel de méthode. Ainsi, vous ne pouvez pas vous fier à l'ordre d'évaluation des paramètres. Évidemment, vous ne pouvez pas surcharger && pour les types POD.
Martin York
3
Vous n'utilisez pas correctement le terme «types de POD». Vous pouvez surcharger && pour n'importe quelle structure, classe, union ou enum, POD ou non. Vous ne pouvez pas surcharger && si les deux côtés sont des types numériques ou des pointeurs.
Derek Ledbetter
J'utilisais POD comme (char / int / float, etc.) et non comme un POD agrégé (ce dont vous parlez) et est généralement référencé séparément ou plus explicitement parce que ce n'est pas un type intégré.
Martin York
2
Donc vous vouliez dire "types fondamentaux" mais avez écrit "types POD"?
Öö Tiib
0

Votre question se résume à la priorité des opérateurs C ++ et à l'associativité. Fondamentalement, dans les expressions avec plusieurs opérateurs et sans parenthèses, le compilateur construit l'arborescence des expressions en suivant ces règles.

Pour la priorité, lorsque vous avez quelque chose comme A op1 B op2 C, vous pouvez regrouper les éléments sous la forme soit (A op1 B) op2 Cou A op1 (B op2 C). Si op1a une priorité plus élevée que op2, vous obtiendrez la première expression. Sinon, vous obtiendrez le deuxième.

Pour l'associativité, lorsque vous avez quelque chose comme A op B op C, vous pouvez à nouveau regrouper les minces comme (A op B) op Cou A op (B op C). Si opa quitté l'associativité, on se retrouve avec la première expression. S'il a la bonne associativité, on se retrouve avec la seconde. Cela fonctionne également pour les opérateurs au même niveau de priorité.

Dans ce cas particulier, &&a une priorité plus élevée que ||, donc l'expression sera évaluée comme (a != "" && it == seqMap.end()) || isEven.

L'ordre lui-même est "de gauche à droite" sur la forme d'arbre d'expression. Nous allons donc d'abord évaluer a != "" && it == seqMap.end(). Si c'est vrai, toute l'expression est vraie, sinon on passe à isEven. La procédure se répète de manière récursive dans la sous-expression de gauche bien sûr.


Quelques informations intéressantes, mais le concept de priorité a ses racines dans la notation mathématique. La même chose se produit dans a*b + c, où *a une priorité plus élevée que +.

Encore plus intéressant / obscur, pour une expression sans parenté A1 op1 A2 op2 ... opn-1 An, où tous les opérateurs ont la même priorité, le nombre d'arbres d'expressions binaires que nous pourrions former est donné par les nombres dits catalans . Pour les grands n, ceux-ci se développent extrêmement rapidement. ré

Horia Coman
la source
Tout cela est correct, mais il s'agit de la priorité des opérateurs et de l'associativité, pas de l'ordre d'évaluation et du court-circuitage. Ce sont des choses différentes.
Thomas Padron-McCarthy
0

Si vous faites confiance à Wikipédia:

[ &&et ||] sont sémantiquement distincts des opérateurs binaires & et | car ils n'évalueront jamais l'opérande de droite si le résultat peut être déterminé à partir de la seule gauche

C (langage de programmation)

Sophie Alpert
la source
11
Pourquoi faire confiance au wiki quand on a un standard!
Martin York
1
Si vous faites confiance à Wikipédia, «Wikipédia n'est pas une ressource fiable» .
Marquis of Lorne
Ceci est vrai pour autant, mais incomplet, car les opérateurs surchargés en C ++ ne sont pas court-circuités.
Thomas Padron-McCarthy