Яка правильна відповідь на cout << a ++ << a ;?


98

Нещодавно в інтерв'ю з'явилося таке об'єктивне питання типу.

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

Відповіді:

а. 10
б. 01
c. невизначена поведінка

Я відповів на вибір 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
Якби це не було визначено, це точно не могло 10б бути, 01або 00. ( c++завжди буде оцінюватися до значення, яке cбуло раніше, ніж збільшуватися). І навіть якщо це не було визначено, воно все одно буде жахливо заплутаним.
Ліворуч близько

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 Порядок уточнення оцінки виразів для Idiomatic C ++ .


@Maxim: Дякую за розширення. З розширеними дзвінками, це буде невизначена поведінка. Але тепер у мене є ще одне запитання (можливо, одни дурне, і мені не вистачає чогось основного, і голосно думаючи) Як ти зробив висновок, що глобальна версія std :: operator << () буде називатися замість ostream :: operator < <() версія члена. Під час налагодження я висаджуюсь у членській версії ostream :: operator << () виклику, а не глобальної версії, і саме тому я спочатку думав, що відповідь буде 01
pravs

@Maxim Не те, що він робить інше, але оскільки cмає тип int, operator<<тут є функції учасників.
Джеймс Канзе

2
@pravs: operator<<функція-член чи вільно стояча функція не впливає на точки послідовності.
Максим Єгорушкін

11
'Точка послідовності' більше не використовується в стандарті C ++. Це було неточним і було замінено на відношення "послідовно попередньо / послідовно після".
Rafał Dowgird

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 Виконання програми [введення.виконання]:
Параграф 17:

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

Зауважте, що тут значення cдоступу отримується не раз без втручальної точки послідовності, щодо цього стандарт говорить:

[Посилання 3] C ++ 03 5 Вирази [expr]:
Параграф 4:

....
Між попередньою та наступною точкою послідовності скалярний об'єкт повинен змінити його збережене значення якнайбільше одночасно оцінкою виразу. Крім того, попереднє значення має доступ лише для визначення значення, яке потрібно зберігати . Вимоги цього пункту повинні бути дотримані для кожного допустимого впорядкування підвиразів повного виразу; інакше поведінка не визначена .

Код змінюється cнеодноразово, не втручаючись точку послідовності, і до нього не можна отримати доступ для визначення значення збереженого об'єкта. Це явне порушення вищезазначеного пункту, а отже, результатом, визначеним стандартом, є Невизначена поведінка [Посилання 3] .


1
Технічно поведінка не визначена, оскільки відбувається модифікація об'єкта та доступ до нього в іншому місці без точки втручання в послідовність. Невизначений не визначений; це залишає реалізацію ще більш легкою.
Джеймс Канзе

1
@ Так. Я не бачив ваших змін (хоча я реагував на заяву jrok про те, що програма не може зробити щось дивне --- це може). Ваша відредагована версія хороша, наскільки це іде, але на мій погляд, ключовим словом є часткове впорядкування ; Точки послідовності вводять лише часткове впорядкування.
Джеймс Канзе

1
@ Дякую за детальний опис, дуже корисний !!
правс

4
Новий стандарт C ++ 0x говорить по суті те саме, але в різних розділах і в різній редакції :) Цитата: (1.9 Виконання програми [введення.виконання], пар. 15): "Якщо побічний ефект на скалярний об'єкт не є наслідком щодо або інший побічний ефект на той самий скалярний об'єкт або обчислення значення, використовуючи значення того ж скалярного об'єкта, поведінка не визначена. "
Rafał Dowgird

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" (перевантажений оператор вважається a виклик функції, і тому є точка послідовності, коли виклик функції повертається). Отже, поведінка і виконання замовлення буде вказано.
Крістофер Сміт

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 ++). Але відповідь повинна відповісти на питання, що вказує, що не так і чому це неправильно. Коментар до питання не є відповіддю, навіть якщо вони абсолютно справедливі. У кращому випадку це може бути коментар, а не відповідь.
ПП
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.