Чому cout друкує "2 + 3 = 15" у цьому фрагменті коду?


126

Чому висновок програми нижче, що це таке?

#include <iostream>
using namespace std;

int main(){

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

виробляє

2+3 = 15

замість очікуваного

2+3 = 5

Це питання вже пройшло кілька циклів закриття / повторного відкриття.

Перш ніж голосувати, щоб закрити, будь ласка, розгляньте цю мета-дискусію щодо цього питання.


96
Ви хочете крапку з комою ;в кінці першого рядка виводу, а не <<. Ви не друкуєте те, що думаєте, що друкуєте. Ви робите cout << cout, який друкує 1(він використовує cout.operator bool(), я думаю). Потім 52+3) негайно випливає, роблячи це схожим на число п’ятнадцять.
Ігор Тандетник

5
@StephanLechner То, мабуть, тоді використовується gcc4. Вони не мали повністю сумісних потоків до gcc5, зокрема, до цього часу вони мали неявне перетворення.
Baum mit Augen

4
@IgorTandetnik, що звучить як початок відповіді. У цьому питанні, як видається, є багато тонкощів, які не очевидні при першому читанні.
Марк Рансом

14
Чому люди продовжують голосувати, щоб закрити це питання? Це не "Будь ласка, скажіть мені, що з цим кодом не так", а "Чому цей код дає такий вихід?" Відповідь на перше - "ви зробили помилку", так, але другий вимагає пояснення того, як компілятор інтерпретує код, чому це не помилка компілятора та як він отримує "1" замість адреси вказівника.
зазубрений шпиль

6
@jaggedSpire Якщо це не типографічна помилка, то це дуже поганий питання, оскільки тоді вона навмисно використовує незвичну конструкцію, схожу на типографічну помилку, не вказуючи, що це навмисно. Так чи інакше, заслуговує на ретельне голосування. (Як через типографічну помилку, чи погано / зловмисно. Це сайт для людей, які шукають допомоги, а не людей, які намагаються обдурити інших.)
David Schwartz

Відповіді:


229

Будь то навмисно чи випадково, у вас є <<в кінці першого рядка вихід, де ви, мабуть, мали на увазі ;. Так ви по суті маєте

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

Тож питання зводиться до цього: чому cout << cout;друкується "1"?

Це виявляється, мабуть напрочуд, тонко. std::coutчерез базовий клас std::basic_iosзабезпечує оператор перетворення певного типу, який призначений для використання в булевому контексті, як у

while (cout) { PrintSomething(cout); }

Це досить бідний приклад, оскільки складно отримати вихід з ладу - але std::basic_iosнасправді базовий клас як для вхідних, так і вихідних потоків, а для введення має набагато більше сенсу:

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

(виходить з циклу в кінці потоку або коли символи потоку не утворюють дійсного цілого числа).

Тепер точне визначення цього оператора перетворення змінилося між C ++ 03 та C ++ 11 версіями стандарту. У старих версіях він був operator void*() const;(як правило, реалізований як return fail() ? NULL : this;), а в більш нових версіях - explicit operator bool() const;як правило, просто як return !fail();). Обидві декларації справно працюють у булевому контексті, але поводяться по-різному, коли (неправильно) використовуються поза таким контекстом.

Зокрема, відповідно до правил C ++ 03, cout << coutінтерпретуватимуться як cout << cout.operator void*()та надрукувати якусь адресу. Згідно з правилами C ++ 11, cout << coutвзагалі не слід збирати компіляцію, оскільки оператор декларується explicitі тому не може брати участь у неявних перетвореннях. Це насправді була основною мотивацією змін - запобігання безглуздого коду від компіляції. Компілятор, який відповідає будь-якому стандарту, не створював би програму, яка друкує "1".

Мабуть, певні реалізації C ++ дозволяють змішувати та узгоджувати компілятор та бібліотеку таким чином, що створює невідповідний результат (цитуючи @StephanLechner: "Я знайшов налаштування в xcode, який виробляє 1, та інше налаштування, яке дає адресу: Мова діалект c ++ 98 у поєднанні зі "Стандартною бібліотекою libc ++ (стандартна бібліотека LLVM з підтримкою c ++ 11)" дає 1, тоді як c ++ 98 у поєднанні з libstdc (gnu c ++ стандартна бібліотека) дає адресу; "). Ви можете мати компілятор стилю C ++ 03, який не розуміє explicitоператорів перетворення (які є новими в C ++ 11) у поєднанні з бібліотекою стилю C ++ 11, яка визначає конверсію як operator bool(). З такою сумішшю стає можливою cout << coutінтерпретація як cout << cout.operator bool(), що, в свою чергу, просто cout << trueі друкує "1".


1
@TC Я впевнений, що в цій конкретній області різниці між C ++ 03 та C ++ 98 немає. Я припускаю, що я міг би замінити всі згадки C ++ 03 на "pre-C ++ 11", якщо це допоможе з'ясувати питання. Я зовсім не знайомий з тонкощами версій компілятора та бібліотеки на Linux та ін; Я хлопець з Windows / MSVC.
Ігор Тандетник

4
Я не намагався вибирати між C ++ 03 і C ++ 98; справа в тому, що libc ++ є лише C ++ 11 і новіше; він не намагається відповідати C ++ 98/03.
ТК

45

Як каже Ігор, ви отримаєте це з C ++ 11 бібліотеки, де std::basic_iosмають ті operator boolзамість operator void*, але як - то не оголошені (або розглядатися як) explicit. Дивіться тут для правильної декларації.

Наприклад, відповідний компілятор C ++ 11 дасть такий самий результат як

#include <iostream>
using namespace std;

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

але у вашому випадку static_cast<bool>істота (помилково) дозволена як неявна конверсія.


Редагувати: Оскільки це не звичайна або очікувана поведінка, може бути корисним знати вашу платформу, версію компілятора тощо.


Редагування 2: Для довідки, код зазвичай записується як

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

або як

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

і це змішування двох стилів разом, які виявили помилку.


1
У вашому першому запропонованому коді рішення є помилка друку. Один занадто багато оператора.
eerorika

3
Зараз я це теж роблю, це повинно бути заразним. Дякую!
Марно

1
Га! :) У початковій редакції своєї відповіді я запропонував додати крапку з комою, але не зрозумів оператора в кінці рядка. Я думаю, що разом з ОП ми створили найбільш значні перестановки друкарських помилок.
eerorika

21

Причиною несподіваного виходу є помилка друку. Ви, мабуть, мали на увазі

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

Якщо ми ігноруємо рядки, які мають очікуваний вихід, нам залишається:

cout << cout;

Оскільки C ++ 11, це неправильно формується. std::coutне неявно перетворюється на все, що std::basic_ostream<char>::operator<<(або перевантаження не є членом). Тому компілятор відповідних стандартів повинен хоча б попереджати вас про це. Мій компілятор відмовився складати вашу програму.

std::coutможе бути конвертованим bool, і перевантаження булів оператора введення потоку матиме спостережуваний вихід 1. Однак це перевантаження явне, тому воно не повинно допускати неявного перетворення. Здається, що ваш компілятор / стандартна реалізація бібліотеки не суворо відповідає стандарту.

У стандарті попереднього С ++ 11 це добре формується. Тоді std::coutмав неявний оператор перетворення, void*якому перевантажено оператор введення потоку. Однак вихід для цього був би іншим. вона б надрукувала адресу пам'яті std::coutоб'єкта.


11

Опублікований код не повинен компілювати для будь-якого C ++ 11 (або пізнішого компілятора), але він повинен компілюватися без попередження навіть про попередні C ++ 11 реалізації.

Різниця полягає в тому, що C ++ 11 зробив перетворення потоку в bool явним:

C.2.15 п. 27: Бібліотека вводу / виводу [diff.cpp03.input.output] 27.7.2.1.3, 27.7.3.4, 27.5.5.4

Зміни: Вкажіть використання явного в існуючих булевих операторах перетворення
Обгрунтування: Уточнюйте наміри, уникайте обхідних шляхів.
Вплив на оригінальну функцію: Дійсний код C ++ 2003, який спирається на неявні булові конверсії, не вдасться компілювати з цим Міжнародним стандартом. Такі перетворення відбуваються в таких умовах:

  • передача значення функції, яка приймає аргумент типу bool;
    ...

ostream оператор << визначається параметром bool. Оскільки перетворення в bool існувало (і не було явним), це попередньо C ++ 11, cout << coutбуло переведено, cout << trueщо дає 1.

Відповідно до C.2.15, це більше не слід компілювати, починаючи з C ++ 11.


3
В boolC ++ 03 не існувало перетворення , однак воно std::basic_ios::operator void*()є значущим як керуючий вираз умовного або циклу.
Бен Войгт

7

Ви можете легко налагодити код таким чином. Коли ви використовуєтеcout ваш вихід буферний, щоб ви могли проаналізувати його так:

Уявіть, що перше виникнення coutявляє собою буфер, а оператор <<представляє додавання до кінця буфера. Результатом оператора <<є вихідний потік у вашому випадку cout. Ви починаєте з:

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

Застосувавши вищезазначені правила, ви отримаєте такий набір дій:

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

Як я вже сказав, результат buffer.append()- буфер. На початку ваш буфер порожній, і у вас є наступне твердження для обробки:

заява: buffer.append("2+3 = ").append(cout).append(2 + 3).append(endl);

буфер: empty

Спочатку у вас є, buffer.append("2+3 = ")який ставить заданий рядок безпосередньо в буфер і стає buffer. Тепер ваш стан виглядає так:

заява: buffer.append(cout).append(2 + 3).append(endl);

буфер: 2+3 = 

Після цього ви продовжуєте аналізувати свою заяву і наштовхуєтесь на coutаргумент, щоб додати її до кінця буфера. Розглядається так, coutяк 1ви додасте1 в кінці вашого буфера. Зараз ви перебуваєте в такому стані:

заява: buffer.append(2 + 3).append(endl);

буфер: 2+3 = 1

Наступне, що у вас є в буфері, 2 + 3і оскільки додавання має більший пріоритет, ніж оператор виводу, ви спочатку додасте ці два числа, а потім ви поставите результат у буфер. Після цього ви отримуєте:

заява: buffer.append(endl);

буфер: 2+3 = 15

Нарешті ви додаєте значення endlв кінці буфера і у вас є:

заява:

буфер: 2+3 = 15\n

Після цього процесу символи з буфера друкуються з буфера на стандартний вихід один за одним. Так що результат вашого коду є 2+3 = 15. Якщо ви подивитесь на це, ви отримаєте додаткове 1від того, що coutви намагалися роздрукувати Видаляючи << coutз заяви, ви отримаєте бажаний результат.


6
Хоча це все правда (і прекрасно відформатована), я думаю, це задає питання. Я вважаю, що питання зводиться до "Чому cout << coutвиробляють 1в першу чергу?" , і ви щойно запевняли, що це відбувається посеред дискусії про ланцюжок операторів вставки.
Марно

1
Хоча +1 за гарне форматування. Враховуючи, що це ваша перша відповідь, приємно, що ви намагаєтесь допомогти :)
gldraphael
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.