Pourquoi cout affiche-t-il «2 + 3 = 15» dans cet extrait de code?

126

Pourquoi la sortie du programme ci-dessous est-elle ce qu'elle est?

#include <iostream>
using namespace std;

int main(){

    cout << "2+3 = " <<
    cout << 2 + 3 << endl;
}

produit

2+3 = 15

au lieu de l'attendu

2+3 = 5

Cette question a déjà subi plusieurs cycles de fermeture / réouverture.

Avant de voter pour la clôture, veuillez considérer cette méta-discussion sur cette question.

Hokhy Tann
la source
96
Vous voulez un point-virgule ;à la fin de la première ligne de sortie, non <<. Vous n'imprimez pas ce que vous pensez imprimer. Vous faites cout << cout, ce qui imprime 1(il utilise cout.operator bool(), je pense). Puis 5(de 2+3) suit immédiatement, faisant ressembler le nombre quinze.
Igor Tandetnik
5
@StephanLechner Cela utilise probablement gcc4 alors. Ils n'avaient pas de flux entièrement conformes avant gcc5, en particulier, ils avaient encore la conversion implicite jusque-là.
Baum mit Augen
4
@IgorTandetnik cela ressemble au début d'une réponse. Il semble y avoir beaucoup de subtilités dans cette question qui ne sont pas évidentes à la première lecture.
Mark Ransom
14
Pourquoi les gens continuent-ils de voter pour clore cette question? Ce n'est pas "Veuillez me dire ce qui ne va pas avec ce code", mais "Pourquoi ce code produit-il cette sortie?" La réponse à la première est "vous avez fait une faute de frappe", oui, mais la seconde nécessite une explication de la façon dont le compilateur interprète le code, pourquoi ce n'est pas une erreur du compilateur et comment il obtient "1" au lieu d'une adresse de pointeur.
jaggedSpire
6
@jaggedSpire Si ce n'est pas une erreur typographique, alors c'est une très mauvaise question car elle utilise délibérément une construction inhabituelle qui ressemble à une erreur typographique sans souligner que c'est intentionnel. Quoi qu'il en soit, cela mérite un vote serré. (Comme dû à une erreur typographique ou mauvais / malveillant. Ceci est un site pour les personnes qui recherchent de l'aide, pas pour les personnes qui essaient de tromper les autres.)
David Schwartz

Réponses:

229

Que ce soit intentionnellement ou par accident, vous avez <<à la fin de la première ligne de sortie, où vous vouliez probablement dire ;. Donc, vous avez essentiellement

cout << "2+3 = ";  // this, of course, prints "2+3 = "
cout << cout;      // this prints "1"
cout << 2 + 3;     // this prints "5"
cout << endl;      // this finishes the line

La question se résume donc à ceci: pourquoi cout << cout;imprimer "1"?

Cela s'avère, peut-être étonnamment, subtil. std::cout, via sa classe de base std::basic_ios, fournit un certain opérateur de conversion de type qui est destiné à être utilisé dans un contexte booléen, comme dans

while (cout) { PrintSomething(cout); }

C'est un exemple assez médiocre, car il est difficile de faire échouer la sortie - mais std::basic_iosc'est en fait une classe de base pour les flux d'entrée et de sortie, et pour l'entrée, cela a beaucoup plus de sens:

int value;
while (cin >> value) { DoSomethingWith(value); }

(sort de la boucle à la fin du flux, ou lorsque les caractères du flux ne forment pas un entier valide).

Désormais, la définition exacte de cet opérateur de conversion a changé entre les versions C ++ 03 et C ++ 11 de la norme. Dans les anciennes versions, il était operator void*() const;(généralement implémenté en tant que return fail() ? NULL : this;), tandis que dans les versions plus récentes, il était explicit operator bool() const;(généralement implémenté simplement en tant que return !fail();). Les deux déclarations fonctionnent correctement dans un contexte booléen, mais se comportent différemment lorsqu'elles sont (mal) utilisées en dehors de ce contexte.

En particulier, sous les règles C ++ 03, cout << coutserait interprété comme cout << cout.operator void*()et afficherait une adresse. Sous les règles de C ++ 11, cout << coutne doit pas du tout compiler, car l'opérateur est déclaré explicitet ne peut donc pas participer aux conversions implicites. C'était en fait la principale motivation du changement - empêcher la compilation de code absurde. Un compilateur conforme à l'une ou l'autre des normes ne produirait pas de programme qui imprime "1".

Apparemment, certaines implémentations C ++ permettent de mélanger et de faire correspondre le compilateur et la bibliothèque de manière à produire un résultat non conforme (citant @StephanLechner: "J'ai trouvé un paramètre dans xcode qui produit 1, et un autre paramètre qui donne une adresse: dialecte de langue c ++ 98 combiné avec "Bibliothèque standard libc ++ (bibliothèque standard LLVM avec support c ++ 11)" donne 1, alors que c ++ 98 combiné avec libstdc (bibliothèque standard gnu c ++) donne une adresse; "). Vous pouvez avoir un compilateur de style C ++ 03 qui ne comprend pas explicitles opérateurs de conversion (qui sont nouveaux dans C ++ 11) combiné avec une bibliothèque de style C ++ 11 qui définit la conversion comme operator bool(). Avec un tel mélange, il devient possible cout << coutd'être interprété comme cout << cout.operator bool(), qui à son tour est simplement cout << trueet imprime "1".

Igor Tandetnik
la source
1
@TC Je suis presque sûr qu'il n'y a pas de différence entre C ++ 03 et C ++ 98 dans ce domaine particulier. Je suppose que je pourrais remplacer toutes les mentions de C ++ 03 par "pre-C ++ 11", si cela pouvait aider à clarifier les choses. Je ne suis pas du tout familier avec les subtilités de la gestion des versions du compilateur et des bibliothèques sur Linux et al; Je suis un gars Windows / MSVC.
Igor Tandetnik
4
Je n'essayais pas de choisir entre C ++ 03 et C ++ 98; le fait est que libc ++ est C ++ 11 et plus récent uniquement; il n'essaye pas de se conformer à C ++ 98/03.
TC
45

Comme le dit Igor, vous obtenez cela avec une bibliothèque C ++ 11, où std::basic_iosa la operator boolplace de operator void*, mais n'est pas déclaré (ou traité comme) explicit. Voir ici pour la déclaration correcte.

Par exemple, un compilateur C ++ 11 conforme donnera le même résultat avec

#include <iostream>
using namespace std;

int main() {
    cout << "2+3 = " << 
    static_cast<bool>(cout) << 2 + 3 << endl;
}

mais dans votre cas, le static_cast<bool>est (à tort) autorisé en tant que conversion implicite.


Edit: Comme ce n'est pas un comportement habituel ou attendu, il peut être utile de connaître votre plate-forme, la version du compilateur, etc.


Edit 2: Pour référence, le code serait généralement écrit soit comme

    cout << "2+3 = "
         << 2 + 3 << endl;

ou comme

    cout << "2+3 = ";
    cout << 2 + 3 << endl;

et c'est le mélange des deux styles qui a exposé le bogue.

Inutile
la source
1
Il y a une faute de frappe dans votre premier code de solution suggéré. Un opérateur de trop.
eerorika
3
Maintenant je le fais aussi, ça doit être contagieux. Merci!
Inutile
1
Ha! :) Dans l'édition initiale de ma réponse, j'ai suggéré d'ajouter le point-virgule, mais je n'ai pas réalisé l'opérateur à la fin de la ligne. Je pense qu'avec OP, nous avons généré les permutations les plus significatives de fautes de frappe que cela peut avoir.
eerorika
21

La raison de la sortie inattendue est une faute de frappe. Vous vouliez probablement

cout << "2+3 = "
     << 2 + 3 << endl;

Si nous ignorons les chaînes qui ont la sortie attendue, nous nous retrouvons avec:

cout << cout;

Depuis C ++ 11, celui-ci est mal formé. std::coutn'est pas implicitement convertible en quelque chose qui std::basic_ostream<char>::operator<<(ou une surcharge non membre) accepterait. Par conséquent, un compilateur conforme aux normes doit au moins vous en avertir. Mon compilateur a refusé de compiler votre programme.

std::coutserait convertible en bool, et la surcharge booléenne de l'opérateur d'entrée de flux aurait la sortie observée de 1. Cependant, cette surcharge est explicite, donc elle ne devrait pas permettre une conversion implicite. Il semble que l'implémentation de votre compilateur / bibliothèque standard ne soit pas strictement conforme à la norme.

Dans un standard pré-C ++ 11, c'est bien formé. À l'époque, il y std::coutavait un opérateur de conversion implicite vers void*lequel a une surcharge d'opérateur d'entrée de flux. La sortie pour cela serait cependant différente. il imprimerait l'adresse mémoire de l' std::coutobjet.

Eerorika
la source
11

Le code publié ne doit pas être compilé pour un compilateur conforme C ++ 11 (ou ultérieur), mais il doit être compilé sans même un avertissement sur les implémentations antérieures à C ++ 11.

La différence est que C ++ 11 a rendu explicite la conversion d'un flux en un booléen:

C.2.15 Clause 27: Bibliothèque d'entrée / sortie [diff.cpp03.input.output] 27.7.2.1.3, 27.7.3.4, 27.5.5.4

Modification: spécifiez l'utilisation d'explicite dans les opérateurs de conversion booléens existants.
Justification: clarifiez les intentions, évitez les solutions de contournement.
Effet sur la fonctionnalité d'origine: le code C ++ 2003 valide qui repose sur des conversions booléennes implicites ne sera pas compilé avec la présente Norme internationale. Ces conversions se produisent dans les conditions suivantes:

  • passer une valeur à une fonction qui prend un argument de type bool;
    ...

L'opérateur ostream << est défini avec un paramètre booléen. Comme une conversion en bool existait (et n'était pas explicite) est pré-C ++ 11, a cout << coutété traduit en cout << truece qui donne 1.

Et selon C.2.15, cela ne devrait plus être compilé à partir de C ++ 11.

Serge Ballesta
la source
3
Aucune conversion booln'existait en C ++ 03, mais il y en a std::basic_ios::operator void*()qui est significatif en tant qu'expression de contrôle d'une condition ou d'une boucle.
Ben Voigt le
7

Vous pouvez facilement déboguer votre code de cette façon. Lorsque vous utilisez coutvotre sortie est mise en mémoire tampon afin que vous puissiez l'analyser comme ceci:

Imaginez que la première occurrence de coutreprésente le tampon et que l'opérateur <<représente l'ajout à la fin du tampon. Le résultat de l'opérateur <<est le flux de sortie, dans votre cas cout. Vous partez de:

cout << "2+3 = " << cout << 2 + 3 << endl;

Après avoir appliqué les règles énoncées ci-dessus, vous obtenez un ensemble d'actions comme celle-ci:

buffer.append("2+3 = ").append(cout).append(2 + 3).append(endl);

Comme je l'ai déjà dit, le résultat buffer.append()est tampon. Au début, votre tampon est vide et vous devez traiter l'instruction suivante:

déclaration: buffer.append("2+3 = ").append(cout).append(2 + 3).append(endl);

tampon: empty

Vous avez d'abord buffer.append("2+3 = ")qui met la chaîne donnée directement dans le tampon et devient buffer. Maintenant, votre état ressemble à ceci:

déclaration: buffer.append(cout).append(2 + 3).append(endl);

tampon: 2+3 = 

Après cela, vous continuez à analyser votre déclaration et vous rencontrez un coutargument à ajouter à la fin du tampon. Le coutest traité comme 1si vous ajouterez 1à la fin de votre tampon. Vous êtes maintenant dans cet état:

déclaration: buffer.append(2 + 3).append(endl);

tampon: 2+3 = 1

La prochaine chose que vous avez dans le tampon est 2 + 3et puisque l'addition a une priorité plus élevée que l'opérateur de sortie, vous ajouterez d'abord ces deux nombres, puis vous placerez le résultat dans le tampon. Après cela, vous obtenez:

déclaration: buffer.append(endl);

tampon: 2+3 = 15

Enfin, vous ajoutez de la valeur endlà la fin du tampon et vous avez:

déclaration:

tampon: 2+3 = 15\n

Après ce processus, les caractères du tampon sont imprimés un par un du tampon vers la sortie standard. Donc, le résultat de votre code est 2+3 = 15. Si vous regardez cela, vous obtenez plus 1de coutvous avez essayé d'imprimer. En supprimant << coutde votre relevé, vous obtiendrez le résultat souhaité.

Ivan Kulezic
la source
6
Bien que tout cela soit vrai (et magnifiquement formaté), je pense que cela pose la question. Je crois que la question se résume à "Pourquoi cout << coutproduit-il 1en premier lieu?" , et vous venez d'affirmer que c'est le cas au milieu d'une discussion sur le chaînage des opérateurs d'insertion.
Inutile
1
+1 pour le beau formatage cependant. Étant donné que c'est votre première réponse, c'est bien que vous
essayiez d'