Каков правильный ответ для cout << a ++ << a ;?

98

Недавно в интервью был задан следующий вопрос объективного типа.

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

Ответы:

а. 10
б. 01
с. неопределенное поведение

Я ответил на вариант b, т.е. вывод будет «01».

Но, к моему удивлению, позже интервьюер сказал мне, что правильный ответ - вариант c: undefined.

Теперь я знаю концепцию точек последовательности в C ++. Поведение не определено для следующего оператора:

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

но в моем понимании для заявления cout << a++ << a, то ostream.operator<<()можно было бы назвать дважды, сначала ostream.operator<<(a++)и позже ostream.operator<<(a).

Я также проверил результат на компиляторе VS2010, и его вывод тоже «01».

Правс
источник
30
Вы просили объяснений? Я часто беседую с потенциальными кандидатами и очень заинтересован в получении вопросов, это проявляет интерес.
Брэди
3
@jrok Это неопределенное поведение. Все, что делает реализация (включая отправку оскорбительного письма от вашего имени вашему боссу), соответствует требованиям.
Джеймс Канце
2
Этот вопрос требует ответа на C ++ 11 ( текущая версия C ++), в котором не упоминаются точки последовательности. К сожалению, я недостаточно осведомлен о замене точек последовательности в C ++ 11.
CB Bailey
3
Если бы это не было undefined, этого определенно не могло быть 10, было бы либо 01или 00. ( c++всегда будет оценивать значение, которое cбыло до увеличения). И даже если бы он не был неопределенным, это все равно было бы ужасно запутанным.
leftaround около
2
Знаете, когда я прочитал заголовок «cout << c ++ << c», я на мгновение подумал об этом как об утверждении отношений между языками C и C ++ и каким-то другим языком с именем «cout». Вы знаете, как кто-то говорил, что они думали, что «cout» намного уступает C ++, и что C ++ намного уступает C - и, вероятно, по транзитивности, «cout» очень, очень уступает C. :)
tchrist

Ответы:

145

Вы можете думать о:

cout << a++ << a;

Так как:

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

C ++ гарантирует, что все побочные эффекты предыдущих оценок будут выполнены в точках последовательности . Между оценками аргументов функции нет точек последовательности, что означает, что аргумент aможет быть оценен до std::operator<<(std::cout, a++)или после аргумента . Таким образом, результат вышеизложенного не определен.


Обновление C ++ 17

В C ++ 17 правила были обновлены. В частности:

В выражении оператора сдвига E1<<E2и E1>>E2каждое вычисление значения и побочный эффект E1упорядочиваются перед каждым вычислением значения и побочным эффектом E2.

Это означает , что он требует код в результате продукции b, которая выводит 01.

Для получения дополнительных сведений см. P0145R3 Порядок оценки уточняющих выражений для идиоматического C ++ .

Максим Егорушкин
источник
@Maxim: Спасибо за разъяснение. С вашими звонками это будет неопределенное поведение. Но теперь у меня есть еще один вопрос (может быть, более строгий, и я упустил что-то базовое и подумал громко) Как вы пришли к выводу, что глобальная версия std :: operator << () будет вызываться вместо ostream :: operator < <() версия члена. При отладке я использую версию-член ostream :: operator << (), а не глобальную версию, и именно поэтому я изначально думал, что ответ будет 01
pravs
@Maxim Не то, чтобы он по-другому, но, поскольку cесть тип int, operator<<здесь представлены функции-члены.
Джеймс Канце
2
@pravs: operator<<функция-член или отдельная функция не влияет на точки последовательности.
Максим Егорушкин
11
«Точка следования» больше не используется в стандарте C ++. Это было неточно и было заменено отношением «последовательность до / последовательность после».
Рафал Довгирд
2
So the result of the above is undefined.Ваше объяснение подходит только для неопределенных , но не для неопределенных . JamesKanze объяснил , как это более убийственный неопределенный в своем ответе , хотя .
Дедупликатор
68

Технически в целом это неопределенное поведение .

Но есть два важных аспекта ответа.

Заявление кода:

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

оценивается как:

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

Стандарт не определяет порядок оценки аргументов функции.
Итак, Либо:

  • std::operator<<(std::cout, a++) оценивается в первую очередь или
  • aоценивается в первую очередь или
  • это может быть любой порядок, определенный реализацией.

Этот порядок не указан [ссылка 1] согласно стандарту.

[Ссылка 1] C ++ 03 5.2.2 Вызов функции,
пункт 8

Порядок оценки аргументов не указан . Все побочные эффекты оценок выражения аргумента вступают в силу до того, как функция будет введена. Порядок вычисления постфиксного выражения и списка выражений аргумента не указан.

Кроме того, нет точки последовательности между оценкой аргументов функции, но точка последовательности существует только после оценки всех аргументов [Ссылка 2] .

[Ссылка 2] C ++ 03 1.9 Выполнение программы [intro.execution]:
Пункт 17:

При вызове функции (независимо от того, является ли функция встроенной), после оценки всех аргументов функции (если есть), которая имеет место перед выполнением любых выражений или операторов в теле функции, существует точка последовательности.

Обратите внимание, что здесь к значению cобращаются более одного раза без промежуточной точки последовательности, относительно этого в стандарте говорится:

[Ссылка 3] C ++ 03 5 Выражения [expr]:
Пункт 4:

....
Между предыдущей и следующей точкой последовательности скалярный объект должен иметь свое сохраненное значение, измененное не более одного раза при оценке выражения. Кроме того, к предыдущему значению необходимо обращаться только для определения значения, которое необходимо сохранить . Требования данного параграфа должны соблюдаться для каждого допустимого порядка подвыражений полного выражения; в противном случае поведение не определено .

Код изменяется cболее одного раза без вмешательства точки последовательности, и к нему не осуществляется доступ для определения значения сохраненного объекта. Это явное нарушение вышеупомянутого пункта и, следовательно, результатом, предписываемым стандартом, является Undefined Behavior [Ref 3] .

Алок Сохранить
источник
1
Технически поведение не определено, потому что существует модификация объекта и доступ к нему в другом месте без промежуточной точки последовательности. Неопределенный не является неопределенным; это оставляет для реализации еще большую свободу действий.
Джеймс Канце
1
@Als Да. Я не видел ваших правок (хотя я реагировал на заявление jrok о том, что программа не может делать что-то странное - может). Ваша отредактированная версия хороша, насколько это возможно, но, на мой взгляд, ключевым словом является частичный порядок ; точки последовательности вводят только частичный порядок.
Джеймс Канце,
1
@Als спасибо за подробное описание, действительно очень полезно !!
правс
4
Новый стандарт C ++ 0x говорит, по сути, то же самое, но в разных разделах и в другой формулировке :) Цитата: (1.9 Выполнение программы [intro.execution], п. 15): «Если побочный эффект на скалярный объект не упорядочен относительно либо другой побочный эффект для того же скалярного объекта, либо вычисление значения с использованием значения того же скалярного объекта, поведение не определено ».
Рафал Довгирд
2
Я считаю, что в этом ответе есть ошибка. "std :: cout << c ++ << c;" невозможно перевести в «std :: operator << (std :: operator << (std :: cout, c ++), c)», потому что std :: operator << (std :: ostream &, int) не существует. Вместо этого он переводится в "std :: cout.operator << (c ++). Operator (c);", который на самом деле имеет точку последовательности между вычислением "c ++" и "c" (перегруженный оператор считается вызов функции и, следовательно, есть точка последовательности, когда вызов функции возвращается). Следовательно, поведение и выполнение заказа будет указано.
Кристофер Смит
20

Точки последовательности определяют только частичный порядок. В вашем случае у вас есть (после разрешения перегрузки):

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

Между ними a++и первым вызовом std::ostream::operator<<есть точка последовательности, а между вторым aи вторым вызовом std::ostream::operator<<есть точка последовательности, но нет точки последовательности между a++и a; единственные ограничения порядка - это то, что они a++должны быть полностью оценены (включая побочные эффекты) перед первым вызовом operator<<, и что второе aдолжно быть полностью оценено перед вторым вызовом operator<<. (Существуют также причинные ограничения порядка: второй вызов operator<<не может предшествовать первому, так как он требует в качестве аргумента результатов первого.) В §5 / 4 (C ++ 03) говорится:

Если не указано иное, порядок оценки операндов отдельных операторов и подвыражений отдельных выражений, а также порядок, в котором имеют место побочные эффекты, не указан. Между предыдущей и следующей точкой последовательности скалярный объект должен иметь свое сохраненное значение, измененное не более одного раза при оценке выражения. Кроме того, к предыдущему значению необходимо обращаться только для определения значения, которое необходимо сохранить. Требования данного параграфа должны соблюдаться для каждого допустимого порядка подвыражений полного выражения; в противном случае поведение не определено.

Один из допустимых упорядочений вашего выражения a++, a, первый вызов operator<<, второй вызов к operator<<; это изменяет сохраненное значение a( a++) и обращается к нему, кроме определения нового значения (второго a), поведение не определено.

Джеймс Канце
источник
Один подвох из вашей цитаты стандарта. «Если не указано иное», IIRC, включает исключение при работе с перегруженным оператором, который рассматривает оператор как функцию и, следовательно, создает точку последовательности между первым и вторым вызовом std :: ostream :: operator << (int ). Пожалуйста, поправьте меня, если я ошибаюсь.
Кристофер Смит
@ChristopherSmith Перегруженный оператор ведет себя как вызов функции. Если cбыл типом пользователя с определенным пользователем ++, вместо того int, результаты были бы не определены, но не было бы неопределенным поведения.
Джеймс Канце
1
@ChristopherSmith Где вы видите точку последовательности между ними cв foo(foo(bar(c)), c)? Есть точка последовательности, когда функции вызываются и когда они возвращаются, но не требуется вызова функции между их оценками c.
Джеймс Канце
1
@ChristopherSmith Если бы cбыл UDT, перегруженные операторы были бы вызовами функций и вводили бы точку последовательности, поэтому поведение не было бы неопределенным. Но все равно не будет указано, было ли cвычислено подвыражение до или после c++, поэтому не будет указываться, получили ли вы увеличенную версию или нет (и теоретически не обязательно должно быть одинаковым каждый раз).
Джеймс Канце
1
@ChristopherSmith Все, что находится до точки последовательности, произойдет до всего, что находится после точки последовательности. Но точки последовательности определяют только частичный порядок. В рассматриваемом выражении, например, нет точки последовательности между подвыражениями cи c++, поэтому они могут встречаться в любом порядке. Что касается точек с запятой ... Они вызывают точку последовательности только в том случае, если являются полными выражениями. Другие важные моменты последовательности вызова функции: f(c++)будет увидеть приращение cв f, а оператор запятой &&, ||а ?:также причина точек последовательности.
Джеймс Канце
4

Правильный ответ - поставить под вопрос вопрос. Утверждение неприемлемо, потому что читатель не видит четкого ответа. Другой способ взглянуть на это состоит в том, что мы ввели побочные эффекты (c ++), которые значительно усложняют интерпретацию этого оператора. Краткий код - это здорово, если его смысл ясен.

Пол Маррингтон
источник
4
Вопрос может свидетельствовать о плохой практике программирования (и даже о недопустимом C ++). Но ответ должен отвечать на вопрос, указывающий, что не так и почему неправильно. Комментарий к вопросу не является ответом, даже если он абсолютно верен. В лучшем случае это может быть комментарий, а не ответ.
PP