Почему 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()мне кажется). Затем сразу следует 5(от 2+3), делая его похожим на число пятнадцать.
Игорь Тандетник
5
@StephanLechner Тогда, вероятно, использовал gcc4. У них не было полностью совместимых потоков до gcc5, в частности, до этого у них все еще использовалось неявное преобразование.
Baum mit Augen
4
@IgorTandetnik, похоже, начало ответа. В этом вопросе есть много тонкостей, которые не очевидны при первом чтении.
Марк Рэнсом
14
Почему люди продолжают голосовать за закрытие этого вопроса? Это не «Пожалуйста, скажите мне, что не так с этим кодом», а «Почему этот код производит такой вывод?» Ответ на первый - «вы допустили опечатку», да, но второй требует объяснения того, как компилятор интерпретирует код, почему это не ошибка компилятора и как он получает «1» вместо адреса указателя.
jaggedSpire
6
@jaggedSpire Если это не типографская ошибка, то это очень плохой вопрос, потому что тогда он намеренно использует необычную конструкцию, которая выглядит как типографская ошибка, не указывая на то, что это намеренно. В любом случае, он заслуживает тщательного голосования. (Как из-за типографской ошибки, так и из-за плохого / злонамеренного. Это сайт для людей, которые ищут помощи, а не для людей, пытающихся обмануть других.)
Дэвид Шварц

Ответы:

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.
TC
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
Ха! :) В первоначальном редактировании своего ответа я предлагал добавить точку с запятой, но не понимал оператора в конце строки. Я думаю, что вместе с OP мы создали наиболее значительную перестановку опечаток, которые могут возникнуть.
eerorika
21

Причина неожиданного вывода - опечатка. Вы, наверное, имели в виду

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

Если мы проигнорируем строки, которые имеют ожидаемый результат, мы останемся с:

cout << cout;

Начиная с C ++ 11, это неправильно. std::coutне может быть неявно преобразован во что-либо, что std::basic_ostream<char>::operator<<(или перегрузка, не являющаяся членом) могла бы принять. Поэтому соответствующий стандартам компилятор должен, по крайней мере, предупредить вас об этом. Мой компилятор отказался компилировать вашу программу.

std::coutбудет преобразовываться в bool, а перегрузка bool оператора потокового ввода будет иметь наблюдаемый результат 1. Однако эта перегрузка является явной, поэтому она не должна допускать неявное преобразование. Похоже, что ваша реализация компилятора / стандартной библиотеки не полностью соответствует стандарту.

В стандарте до C ++ 11 это хорошо сформировано. Тогда std::coutбыл оператор неявного преобразования, в void*который была перегрузка оператора потокового ввода. Однако результат для этого будет другим. он напечатал бы адрес памяти std::coutобъекта.

eerorika
источник
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 operator << определяется с помощью параметра bool. Поскольку преобразование в bool существовало (и не было явным) было до C ++ 11, cout << coutбыло переведено наcout << true которое дает 1.

И согласно C.2.15, это больше не должно компилироваться, начиная с C ++ 11.

Серж Бальеста
источник
3
Преобразования boolв C ++ 03 не существовало, однако есть такое, std::basic_ios::operator void*()которое имеет смысл в качестве управляющего выражения условия или цикла.
Ben Voigt
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, и вы только что заявили, что это так, в середине обсуждения цепочки операторов вставки.
Useless
1
+1 хотя за красивое форматирование. Учитывая, что это ваш первый ответ, приятно, что вы пытаетесь помочь :)
gldraphael