Dlaczego cout drukuje „2 + 3 = 15” w tym fragmencie kodu?

126

Dlaczego wynik poniższego programu jest taki, jaki jest?

#include <iostream>
using namespace std;

int main(){

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

produkuje

2+3 = 15

zamiast oczekiwanego

2+3 = 5

To pytanie przeszło już wiele cykli zamykania / ponownego otwierania.

Przed głosowaniem za zamknięciem prosimy o rozważenie tej meta dyskusji na ten temat.

Hokhy Tann
źródło
96
Chcesz, aby średnik znajdował się ;na końcu pierwszego wiersza wyjścia, a nie <<. Nie drukujesz tego, co myślisz, że drukujesz. Robisz cout << cout, co drukuje 1( cout.operator bool()chyba używa ). Następnie 5(od 2+3) następuje natychmiast, dzięki czemu wygląda jak liczba piętnaście.
Igor Tandetnik
5
@StephanLechner Prawdopodobnie używa się wtedy gcc4. Nie mieli w pełni zgodnych strumieni do czasu gcc5, w szczególności nadal mieli niejawną konwersję do tego czasu.
Baum mit Augen
4
@IgorTandetnik to brzmi jak początek odpowiedzi. Wydaje się, że jest wiele subtelności w tym pytaniu, które nie są oczywiste przy pierwszym czytaniu.
Mark Ransom
14
Dlaczego ludzie wciąż głosują za zamknięciem tego pytania? To nie jest „Proszę powiedz mi, co jest nie tak z tym kodem”, ale „Dlaczego ten kod generuje takie dane wyjściowe?” Odpowiedź na pierwsze pytanie brzmi: „popełniłeś literówkę”, ale druga wymaga wyjaśnienia, w jaki sposób kompilator interpretuje kod, dlaczego nie jest to błąd kompilatora i jak otrzymuje „1” zamiast adresu wskaźnika.
jaggedSpire
6
@jaggedSpire Jeśli nie jest to błąd typograficzny, to jest to bardzo złe pytanie, ponieważ celowo używa nietypowej konstrukcji, która wygląda jak błąd typograficzny, bez wskazania, że ​​jest to zamierzone. Tak czy inaczej, zasługuje na bliskie głosowanie. (. Ponieważ zarówno z powodu błędu typograficznego lub złe / złośliwy Jest to miejsce dla osób szukających pomocy, a nie ludzi, którzy próbują oszukać innych.)
David Schwartz

Odpowiedzi:

229

Czy celowo, czy przypadkowo, masz <<na końcu pierwszego wiersza wyjścia, gdzie prawdopodobnie miałeś na myśli ;. Więc zasadniczo masz

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

A więc pytanie sprowadza się do tego: dlaczego cout << cout;druk "1"?

Okazuje się to, być może zaskakująco, subtelne. std::cout, poprzez swoją klasę bazową std::basic_ios, zapewnia pewien operator konwersji typu, który jest przeznaczony do użycia w kontekście logicznym, jak w

while (cout) { PrintSomething(cout); }

To dość kiepski przykład, ponieważ trudno jest doprowadzić do niepowodzenia wyjścia - ale w std::basic_iosrzeczywistości jest to klasa bazowa zarówno dla strumieni wejściowych, jak i wyjściowych, a dla danych wejściowych ma znacznie więcej sensu:

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

(wychodzi z pętli na końcu strumienia lub gdy znaki strumienia nie tworzą prawidłowej liczby całkowitej).

Teraz dokładna definicja tego operatora konwersji została zmieniona między wersjami standardu C ++ 03 i C ++ 11. W starszych wersjach był operator void*() const;(zwykle implementowany jako return fail() ? NULL : this;), podczas gdy w nowszych jest explicit operator bool() const;(zwykle implementowany po prostu jako return !fail();). Obie deklaracje działają dobrze w kontekście logicznym, ale zachowują się inaczej, gdy (niewłaściwie) są używane poza tym kontekstem.

W szczególności, zgodnie z regułami C ++ 03, cout << coutzostanie zinterpretowany jako cout << cout.operator void*()i wypisze jakiś adres. Zgodnie z regułami C ++ 11 cout << coutnie powinien w ogóle kompilować, ponieważ operator jest zadeklarowany, explicita zatem nie może uczestniczyć w niejawnych konwersjach. To była w istocie główna motywacja do zmiany - zapobieżenie kompilacji bezsensownego kodu. Kompilator zgodny z żadnym standardem nie utworzyłby programu, który drukuje "1".

Najwyraźniej niektóre implementacje C ++ pozwalają na miksowanie i dopasowywanie kompilatora i biblioteki w sposób, który daje niezgodny wynik (cytując @StephanLechner: „Znalazłem ustawienie w xcode, które daje 1, i inne ustawienie, które daje adres: Dialekt języka c ++ 98 w połączeniu z "Biblioteka standardowa libc ++ (biblioteka standardowa LLVM z obsługą c ++ 11)" daje 1, podczas gdy c ++ 98 w połączeniu z libstdc (biblioteka standardowa gnu c ++) daje adres; "). Możesz mieć kompilator w stylu C ++ 03, który nie rozumie explicitoperatorów konwersji (które są nowe w C ++ 11) w połączeniu z biblioteką w stylu C ++ 11, która definiuje konwersję jako operator bool(). Z taką mieszanką staje się możliwe cout << coutzinterpretowanie jako cout << cout.operator bool(), co z kolei jest proste cout << truei drukuje "1".

Igor Tandetnik
źródło
1
@TC Jestem prawie pewien, że nie ma różnicy między C ++ 03 i C ++ 98 w tym konkretnym obszarze. Przypuszczam, że mógłbym zamienić wszystkie wzmianki o C ++ 03 na „pre-C ++ 11”, jeśli to pomogłoby w wyjaśnieniu sprawy. Wcale nie jestem zaznajomiony z zawiłościami kompilatora i wersjonowania bibliotek w Linuksie i in .; Jestem facetem od Windows / MSVC.
Igor Tandetnik
4
Nie próbowałem szukać między C ++ 03 i C ++ 98; chodzi o to, że libc ++ to tylko C ++ 11 i nowsze; nie próbuje dostosować się do C ++ 98/03.
TC
45

Jak mówi Igor, to masz z biblioteki C ++ 11, gdzie std::basic_iosma operator boolzamiast operator void*, ale jakoś nie jest zadeklarowana (lub traktowane jako) explicit. Patrz tutaj dla prawidłowej deklaracji.

Na przykład zgodny kompilator C ++ 11 da ten sam wynik z

#include <iostream>
using namespace std;

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

ale w twoim przypadku static_cast<bool>jest (niesłusznie) dozwolona jako niejawna konwersja.


Edycja: ponieważ nie jest to zwykłe lub oczekiwane zachowanie, warto znać swoją platformę, wersję kompilatora itp.


Edycja 2: w celach informacyjnych kod byłby zwykle zapisywany jako

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

lub jako

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

i to mieszanie tych dwóch stylów razem ujawniło błąd.

Bezużyteczny
źródło
1
W pierwszym sugerowanym kodzie rozwiązania jest błąd. O jeden operator za dużo.
eerorika
3
Teraz też to robię, to musi być zaraźliwe. Dzięki!
Bezużyteczne
1
Ha! :) W początkowej edycji mojej odpowiedzi zasugerowałem dodanie średnika, ale nie zdawałem sobie sprawy z operatora na końcu linii. Myślę, że razem z OP wygenerowaliśmy najbardziej znaczące permutacje literówek, jakie może to mieć.
eerorika
21

Przyczyną nieoczekiwanego wyniku jest literówka. Prawdopodobnie miałeś na myśli

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

Jeśli zignorujemy łańcuchy, które mają oczekiwany wynik, zostanie nam:

cout << cout;

Od C ++ 11 jest to źle sformułowane. std::coutnie jest niejawnie konwertowany na cokolwiek, co std::basic_ostream<char>::operator<<(lub przeciążenie niebędące składnikiem) zaakceptowałoby. Dlatego kompilator zgodny ze standardami musi przynajmniej ostrzec Cię przed tym. Mój kompilator odmówił skompilowania twojego programu.

std::coutbyłoby konwertowane na bool, a przeciążenie bool operatora wejściowego strumienia miałoby obserwowane wyjście równe 1. Jednak to przeciążenie jest jawne, więc nie powinno zezwalać na niejawną konwersję. Wygląda na to, że implementacja Twojego kompilatora / biblioteki standardowej nie jest ściśle zgodna ze standardem.

W standardzie poprzedzającym C ++ 11 jest to dobrze sformułowane. Wtedy std::coutmiał niejawny operator konwersji, void*który ma przeciążenie operatora wejściowego strumienia. Wynik dla tego byłby jednak inny. wypisuje adres pamięci std::coutobiektu.

eerorika
źródło
11

Opublikowany kod nie powinien kompilować się dla żadnego C ++ 11 (lub nowszego zgodnego kompilatora), ale powinien kompilować się nawet bez ostrzeżenia o implementacjach starszych niż C ++ 11.

Różnica polega na tym, że C ++ 11 dokonał konwersji strumienia na jawną wartość bool:

C.2.15 Punkt 27: Biblioteka wejścia / wyjścia [diff.cpp03.input.output] 27.7.2.1.3, 27.7.3.4, 27.5.5.4

Zmiana: Określ użycie jawnego w istniejących operatorach konwersji logicznej
Uzasadnienie: Wyjaśnij zamiary, unikaj obejść.
Wpływ na oryginalną funkcję: poprawny kod C ++ 2003, który opiera się na niejawnych konwersjach boolowskich, nie zostanie skompilowany z tym standardem międzynarodowym. Takie konwersje mają miejsce w następujących warunkach:

  • przekazanie wartości do funkcji, która przyjmuje argument typu bool;
    ...

operator ostream << jest definiowany za pomocą parametru bool. Ponieważ konwersja do bool istniała (i nie była jawna) była wcześniejsza niż C ++ 11, cout << coutzostała przetłumaczona na, cout << trueco daje 1.

Zgodnie z C.2.15 nie powinno to już kompilować się od C ++ 11.

Serge Ballesta
źródło
3
boolW C ++ 03 nie istniała żadna konwersja , ale jest to, std::basic_ios::operator void*()co ma znaczenie jako kontrolujące wyrażenie warunku lub pętli.
Ben Voigt
7

W ten sposób możesz łatwo debugować swój kod. Kiedy używasz coutdane wyjściowe, są buforowane, więc możesz je analizować w następujący sposób:

Wyobraź sobie, że pierwsze wystąpienie coutreprezentuje bufor, a operator <<reprezentuje dołączenie do końca bufora. <<W twoim przypadku wynikiem operatora jest strumień wyjściowy cout. Zaczynasz od:

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

Po zastosowaniu powyższych zasad otrzymasz zestaw działań takich jak:

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

Jak powiedziałem wcześniej, wynikiem buffer.append()jest bufor. Na początku twój bufor jest pusty i musisz przetworzyć następującą instrukcję:

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

bufor: empty

Najpierw masz, buffer.append("2+3 = ")który umieszcza dany ciąg bezpośrednio w buforze i staje się buffer. Teraz twój stan wygląda tak:

komunikat: buffer.append(cout).append(2 + 3).append(endl);

bufor: 2+3 = 

Następnie kontynuujesz analizę swojego oświadczenia i natrafisz na coutargument do dołączenia na końcu bufora. coutJest traktowany jak 1tak można dołączyć 1do końca swojego bufora. Teraz jesteś w tym stanie:

komunikat: buffer.append(2 + 3).append(endl);

bufor: 2+3 = 1

Następną rzeczą, którą masz w buforze, jest to, 2 + 3że dodawanie ma wyższy priorytet niż operator wyjściowy, najpierw dodasz te dwie liczby, a następnie umieścisz wynik w buforze. Po tym otrzymasz:

komunikat: buffer.append(endl);

bufor: 2+3 = 15

Na koniec dodajesz wartość endldo końca bufora i masz:

komunikat:

bufor: 2+3 = 15\n

Po tym procesie znaki z bufora są kolejno wypisywane z bufora na standardowe wyjście. Więc wynik twojego kodu to 2+3 = 15. Jeśli spojrzysz na to, dostaniesz dodatkowe 1od coutpróby drukowania. Usuwając << coutz oświadczenia, uzyskasz pożądany wynik.

Ivan Kulezic
źródło
6
Chociaż to wszystko prawda (i pięknie sformatowane), myślę, że rodzi pytanie. Myślę, że pytanie sprowadza się do „Dlaczego przede wszystkim cout << coutprodukuje 1?” , i właśnie zapewniłeś, że tak jest w środku dyskusji na temat tworzenia łańcuchów operatorów wstawiania.
Bezużyteczne
1
Jednak +1 za piękne formatowanie. Biorąc pod uwagę, że to Twoja pierwsza odpowiedź, miło, że próbujesz pomóc :)
gldraphael