Quelle est la bonne réponse pour cout << a ++ << a ;?

98

Récemment, dans une interview, il y avait une question de type objectif suivante.

int a = 0;
cout << a++ << a;

Réponses:

une. 10
b. 01
ch. comportement indéfini

J'ai répondu au choix b, c'est-à-dire que la sortie serait "01".

Mais à ma grande surprise plus tard, un intervieweur m'a dit que la bonne réponse était l'option c: indéfinie.

Maintenant, je connais le concept de points de séquence en C ++. Le comportement n'est pas défini pour l'instruction suivante:

int i = 0;
i += i++ + i++;

mais selon ma compréhension de la déclaration cout << a++ << a, le ostream.operator<<()serait appelé deux fois, d'abord avec ostream.operator<<(a++)et plus tard ostream.operator<<(a).

J'ai également vérifié le résultat sur le compilateur VS2010 et sa sortie est également «01».

pravs
la source
30
Avez-vous demandé une explication? J'interviewe souvent des candidats potentiels et je suis très intéressé à recevoir des questions, cela montre de l'intérêt.
Brady
3
@jrok C'est un comportement indéfini. Tout ce que fait la mise en œuvre (y compris l'envoi d'un e-mail insultant en votre nom à votre patron) est conforme.
James Kanze
2
Cette question demande une réponse C ++ 11 (la version actuelle de C ++) qui ne mentionne pas les points de séquence. Malheureusement, je ne connais pas assez bien le remplacement des points de séquence dans C ++ 11.
CB Bailey
3
Si ce n'était pas indéfini, cela ne pourrait certainement pas être 10, ce serait soit 01ou 00. ( c++Toujours évaluer à la valeur cavait avant d' être incrémentée). Et même si ce n'était pas indéfini, ce serait encore horriblement déroutant.
gauche vers
2
Tu sais, quand j'ai lu le titre «cout << c ++ << c», j'y ai momentanément pensé comme une déclaration sur la relation entre les langages C et C ++, et une autre nommée «cout». Vous savez, comme quelqu'un disait comment ils pensaient que «cout» était bien inférieur au C ++, et que C ++ était bien inférieur au C - et probablement par transitivité que «cout» était très, très inférieur à C. :)
tchrist

Réponses:

145

Vous pouvez penser à:

cout << a++ << a;

Comme:

std::operator<<(std::operator<<(std::cout, a++), a);

C ++ garantit que tous les effets secondaires des évaluations précédentes auront été exécutés aux points de séquence . Il n'y a pas de points de séquence entre l'évaluation des arguments de fonction, ce qui signifie que l'argument apeut être évalué avant std::operator<<(std::cout, a++)ou après l' argument . Donc, le résultat de ce qui précède n'est pas défini.


Mise à jour C ++ 17

En C ++ 17, les règles ont été mises à jour. En particulier:

Dans une expression d'opérateur de décalage E1<<E2et E1>>E2, chaque calcul de valeur et effet secondaire de E1est séquencé avant chaque calcul de valeur et effet secondaire de E2.

Ce qui signifie qu'il faut que le code produise un résultat b, qui sort 01.

Voir P0145R3 Raffinage de l'ordre d'évaluation des expressions pour Idiomatic C ++ pour plus de détails.

Maxim Egorushkin
la source
@Maxim: Merci pour l'expansion. Avec les appels que vous avez passés, ce serait un comportement indéfini. Mais maintenant, j'ai une autre question (peut-être une question insignifiante, et il me manque quelque chose de basique et de réflexion) Comment avez-vous déduit que la version globale de std :: operator << () serait appelée au lieu de ostream :: operator < <() version du membre. Lors du débogage, j'atterris dans une version membre de l'appel ostream :: operator << () plutôt que dans la version globale et c'est la raison pour laquelle j'ai initialement pensé que la réponse serait 01
pravs
@Maxim Non pas que cela fasse un différent, mais depuis ca type int, les operator<<voici des fonctions membres.
James Kanze
2
@pravs: qu'il s'agisse d' operator<<une fonction membre ou d'une fonction autonome n'affecte pas les points de séquence.
Maxim Egorushkin
11
Le «point de séquence» n'est plus utilisé dans la norme C ++. Elle était imprécise et a été remplacée par la relation «séquencé avant / séquencé après».
Rafał Dowgird
2
So the result of the above is undefined.Votre explication n'est valable que pour non spécifié , pas pour indéfini . JamesKanze a expliqué comment c'était le plus indéfini dans sa réponse .
Deduplicator
68

Techniquement, dans l'ensemble, il s'agit d'un comportement indéfini .

Mais, il y a deux aspects importants à la réponse.

L'instruction de code:

std::cout << a++ << a;

est évalué comme:

std::operator<<(std::operator<<(std::cout, a++), a);

La norme ne définit pas l'ordre d'évaluation des arguments d'une fonction.
Donc soit:

  • std::operator<<(std::cout, a++) est évalué en premier ou
  • aest évalué en premier ou
  • il peut s'agir de n'importe quel ordre défini par l'implémentation.

Cette commande n'est pas spécifiée [Ref 1] selon la norme.

[Ref 1] C ++ 03 5.2.2 Appel de fonction
Para 8

L'ordre d'évaluation des arguments n'est pas spécifié . Tous les effets secondaires des évaluations d'expressions d'argument prennent effet avant que la fonction ne soit entrée. L'ordre d'évaluation de l'expression postfix et de la liste des expressions d'argument n'est pas spécifié.

De plus, il n'y a pas de point de séquence entre l'évaluation des arguments d'une fonction mais un point de séquence n'existe qu'après évaluation de tous les arguments [Ref 2] .

[Ref 2] C ++ 03 1.9 Exécution du programme [intro.execution]:
Para 17:

Lors de l'appel d'une fonction (que la fonction soit en ligne ou non), il existe un point de séquence après l'évaluation de tous les arguments de fonction (le cas échéant) qui a lieu avant l'exécution de toute expression ou instruction dans le corps de la fonction.

Notez que, ici, la valeur de cest accédée plus d'une fois sans point de séquence intermédiaire, à ce sujet, la norme dit:

[Réf 3] C ++ 03 5 Expressions [expr]:
Para 4:

....
Entre le point de séquence précédent et suivant, un objet scalaire verra sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression. De plus, la valeur précédente ne sera accessible que pour déterminer la valeur à stocker . Les exigences de ce paragraphe doivent être remplies pour chaque ordre autorisé des sous-expressions d'une expression complète; sinon, le comportement n'est pas défini .

Le code est modifié cplus d'une fois sans point de séquence intervenant et il n'est pas accédé pour déterminer la valeur de l'objet stocké. C'est une violation flagrante de la clause ci-dessus et par conséquent, le résultat tel que prescrit par la norme est un comportement indéfini [Réf 3] .

Alok Save
la source
1
Techniquement, le comportement est indéfini, car il y a modification d'un objet, et y accéder ailleurs sans point de séquence intermédiaire. Undefined n'est pas non spécifié; cela laisse encore plus de marge de manœuvre à la mise en œuvre.
James Kanze
1
@Als Oui. Je n'avais pas vu vos modifications (même si je réagissais à la déclaration de jrok selon laquelle le programme ne peut pas faire quelque chose de bizarre - il le peut). Votre version éditée est bonne dans la mesure où elle disparaît, mais dans mon esprit, le mot clé est la commande partielle ; les points de séquence n'introduisent qu'un ordre partiel.
James Kanze
1
@Als merci pour une description élaborée, vraiment très utile !!
pravs
4
Le nouveau standard C ++ 0x dit essentiellement la même chose mais dans des sections différentes et dans des termes différents :) Citation: (1.9 Exécution de programme [intro.execution], par 15): "Si un effet secondaire sur un objet scalaire n'est pas séquencé par rapport soit un autre effet secondaire sur le même objet scalaire, soit un calcul de valeur utilisant la valeur du même objet scalaire, le comportement est indéfini. "
Rafał Dowgird
2
Je crois qu'il y a un bug dans cette réponse. "std :: cout << c ++ << c;" ne peut pas se traduire en "std :: operator << (std :: operator << (std :: cout, c ++), c)", car std :: operator << (std :: ostream &, int) n'existe pas. Au lieu de cela, il se traduit par "std :: cout.operator << (c ++). Operator (c);", qui a en fait un point de séquence entre l'évaluation de "c ++" et "c" (un opérateur surchargé est considéré comme un appel de fonction et donc il y a un point de séquence lorsque l'appel de fonction revient). Par conséquent, le comportement et l'ordre d'exécution sont spécifiés.
Christopher Smith
20

Les points de séquence ne définissent qu'un ordre partiel . Dans votre cas, vous avez (une fois la résolution de surcharge effectuée):

std::cout.operator<<( a++ ).operator<<( a );

Il y a un point de séquence entre le a++et le premier appel à std::ostream::operator<<, et il y a un point de séquence entre le deuxième aet le deuxième appel à std::ostream::operator<<, mais il n'y a pas de point de séquence entre a++et a; les seules contraintes d'ordre sont celles qui doivent a++être pleinement évaluées (y compris les effets secondaires) avant le premier appel à operator<<, et que la seconde asoit entièrement évaluée avant le deuxième appel à operator<<. (Il existe également des contraintes d'ordre causal: le deuxième appel à operator<<ne peut pas précéder le premier, car il nécessite les résultats du premier comme argument.) §5 / 4 (C ++ 03) déclare:

Sauf indication contraire, l'ordre d'évaluation des opérandes d'opérateurs individuels et des sous-expressions d'expressions individuelles, ainsi que l'ordre dans lequel les effets secondaires se produisent, n'est pas spécifié. Entre le point de séquence précédent et suivant, un objet scalaire verra sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression. De plus, la valeur précédente ne sera accessible que pour déterminer la valeur à stocker. Les exigences de ce paragraphe doivent être remplies pour chaque ordre autorisé des sous-expressions d'une expression complète; sinon, le comportement n'est pas défini.

L' une des ordonnancements admissibles de votre expression est a++, ad'abord appel à operator<<, deuxième appel à operator<<; cela modifie la valeur stockée de a( a++), et y accède autrement que pour déterminer la nouvelle valeur (la seconde a), le comportement est indéfini.

James Kanze
la source
Une capture de votre citation de la norme. Le "sauf indication contraire", IIRC, inclut une exception lorsqu'il s'agit d'un opérateur surchargé, qui traite l'opérateur comme une fonction et crée donc un point de séquence entre le premier et le deuxième appel à std :: ostream :: operator << (int ). S'il vous plait corrigez moi si je me trompe.
Christopher Smith
@ChristopherSmith Un opérateur surchargé se comporte comme un appel de fonction. S'il y cavait un type d'utilisateur avec un utilisateur défini ++, au lieu de int, les résultats ne seraient pas spécifiés, mais il n'y aurait pas de comportement indéfini.
James Kanze
1
@ChristopherSmith Où voyez-vous un point de séquence entre les deux c en foo(foo(bar(c)), c)? Il y a un point de séquence lorsque les fonctions sont appelées et quand elles reviennent, mais aucun appel de fonction n'est requis entre les évaluations des deux c.
James Kanze
1
@ChristopherSmith Si cétait un UDT, les opérateurs surchargés seraient des appels de fonction et introduiraient un point de séquence, de sorte que le comportement ne serait pas indéfini. Mais il ne serait toujours pas spécifié si la sous-expression a cété évaluée avant ou aprèsc++ , donc si vous avez la version incrémentée ou non ne serait pas spécifié (et en théorie, ne devrait pas être la même à chaque fois).
James Kanze
1
@ChristopherSmith Tout avant le point de séquence se produira avant tout après le point de séquence. Mais les points de séquence ne définissent qu'un ordre partiel. Dans l'expression en question, par exemple, il n'y a pas de point de séquence entre les sous-expressions cet c++, donc les deux peuvent apparaître dans n'importe quel ordre. Quant aux points-virgules ... Ils ne provoquent un point de séquence que dans la mesure où ce sont des expressions complètes. D'autres points de séquence importants sont l'appel de fonction: f(c++)verra l'incrémenté cdans f, et l'opérateur virgule &&, ||et ?:provoquera également des points de séquence.
James Kanze le
4

La bonne réponse est de remettre en question la question. La déclaration est inacceptable car un lecteur ne peut pas voir une réponse claire. Une autre façon de voir les choses est que nous avons introduit des effets secondaires (c ++) qui rendent la déclaration beaucoup plus difficile à interpréter. Un code concis est excellent, à condition que sa signification soit claire.

Paul Marrington
la source
4
La question peut présenter une mauvaise pratique de programmation (et même un C ++ invalide). Mais une réponse est censée répondre à la question indiquant ce qui ne va pas et pourquoi c'est faux. Un commentaire sur la question n'est pas une réponse même s'il est parfaitement valable. Au mieux, cela peut être un commentaire, pas une réponse.
PP