Чому потрійний оператор комами оцінює лише один вираз у справжньому випадку?


119

Зараз я навчаюсь C ++ із книгою C ++ Primer і однією з вправ у книзі є:

Поясніть, що робить наступний вираз: someValue ? ++x, ++y : --x, --y

Що ми знаємо? Ми знаємо, що потрійний оператор має вищий пріоритет, ніж оператор комами. З бінарними операторами це було досить просто зрозуміти, але з потрійним оператором я борюся трохи. Для двійкових операторів ", що мають більший пріоритет" означає, що ми можемо використовувати дужки навколо виразу з більш високим пріоритетом, і це не змінить виконання.

Для потрійного оператора я би зробив:

(someValue ? ++x, ++y : --x, --y)

ефективно виходить той самий код, який не допомагає мені зрозуміти, як компілятор буде групувати код.

Однак, випробовуючи компілятор C ++, я знаю, що вираз компілюється, і я не знаю, що :може стояти сам оператор. Отже, компілятор, здається, правильно інтерпретує термінальний оператор.

Потім я виконав програму двома способами:

#include <iostream>

int main()
{
    bool someValue = true;
    int x = 10, y = 10;

    someValue ? ++x, ++y : --x, --y;

    std::cout << x << " " << y << std::endl;
    return 0;
}

Призводить до:

11 10

Хоча з іншого боку на someValue = falseній друкується:

9 9

Чому компілятор C ++ генерує код, який для істинної гілки потрійного оператора збільшується лише з кроком x, тоді як для хибної гілки потрійного він зменшує як xі y?

Я навіть пішов так, щоб ставити дужки навколо справжньої гілки так:

someValue ? (++x, ++y) : --x, --y;

але це все одно призводить до 11 10.


5
"Прецедент" - це лише нове явище в С ++. Може бути простіше просто подивитися на граматику мови та побачити, як працюють вирази.
Керрек СБ

26
Нам не важливо , що багато про принципи. :-) Те, що вам потрібно це запитати тут, означає, що код ніколи не пройде перевірку коду вашими колегами-програмістами. Це робить знання про те, як це насправді працює, менш корисні. Якщо , звичайно, ви не хочете брати участь у конкурсі « Замучений С» .
Бо Персон

5
@BoPersson без таких прикладів, з яких можна дізнатися, майбутні рецензенти ніколи не дізнаються, чому вони повинні відхиляти це від виробництва.
Левшенко

8
@ Левшенко - у будь-якому випадку повинні дзвонити попереджувальні дзвони. Кілька приростів і декрементів в одному і тому ж висловлюванні (дінг, дінг, дінь!). Потрійний оператор, коли ви могли використовувати інше (ding, ding, ding!). Зачекайте, чи є ці коми через жахливий оператор комами? (Дінг, ДІНГ, ДІНГ!) З усіма цими операторами, чи може бути щось пріоритетне? (дінг, дінг, дінь!) Тому ми ніколи не зможемо цим скористатися. Тоді навіщо витрачати час на з'ясування того, що це робить, якщо що?
Бо Персон

4
Незначна нитка: назва ?- умовний оператор . Термін термінальний оператор просто означає оператор з трьома операндами. Умовний оператор є одним із прикладів потрійного оператора, але мова (теоретично) може мати декілька потрійних операторів.
бта

Відповіді:


122

Як сказала @Rakete у своїй чудовій відповіді, це хитро. Я хотів би трохи додати до цього.

Потрійний оператор повинен мати форму:

логіко-або-вираз ? вираз : присвоювання вираз

Отже, у нас є такі відображення:

  • someValue: логічне чи вираження
  • ++x, ++y: вираз
  • ??? це призначення-вираз --x, --y чи тільки --x?

Насправді це лише --xтому, що вираз призначення не може бути проаналізований як два вирази, розділені комою (згідно з граматичними правилами C ++), тому --x, --yне може розглядатися як вираз призначення .

У результаті чого частина потрійного (умовного) виразу виглядає так:

someValue?++x,++y:--x

Це може допомогти для читабельності вважати ++x,++yобчисленими так, як якщо б у дужках не було (++x,++y); все, що міститься між ?і :буде послідовно послідовно після умовного. (Я буду їх дужкою в решті публікації).

і оцінюється в такому порядку:

  1. someValue?
  2. (++x,++y)або --x(залежно від boolрезультату 1.)

Потім цей вираз трактується як лівий підвираз оператору коми, при цьому правий підвираз має такий вигляд --y:

(someValue?(++x,++y):--x), --y;

Що означає, що ліва сторона є виразом відкинутого значення , це означає, що вона точно оцінюється, але тоді ми оцінюємо праву сторону і повертаємо її.

Отже, що відбувається, коли someValueє true?

  1. (someValue?(++x,++y):--x)виконує і збільшує xі yбути 11і11
  2. Лівий вираз відкидається (хоча побічні ефекти приросту залишаються)
  3. Оцінюємо праву частину оператора кома:, --yяка потім yповертається до декрементів10

Щоб "виправити" поведінку, ви можете згрупувати --x, --yкруглі дужки, щоб перетворити її в первинний вираз, який є дійсним записом для призначення-вираз *:

someValue?++x,++y:(--x, --y);

* Це досить смішний довгий ланцюг, який з'єднує присвоєння-вираз назад до основного виразу:

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


10
Дякуємо за те, що вирішили розгадати правила граматики; це показує, що в граматиці С ++ є більше, ніж ви знайдете в більшості підручників.
sdenham

4
@sdenham: Коли люди запитують, чому "мови, орієнтовані на вираз", добре (тобто коли { ... }можна трактувати як вираз), у мене зараз є відповідь => це уникати необхідності вводити оператор комами, який веде себе такими хитрими способами.
Матьє М.

Не могли б ви дати мені посилання, щоб прочитати про assignment-expressionланцюжок?
МіП

@MiP: Я підняв його із самого стандарту, ви можете знайти його під gram.expr
AndyG

88

Нічого собі, це хитро.

Компілятор розглядає ваше вираження як:

(someValue ? (++x, ++y) : --x), --y;

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

Тепер це може мати більше сенсу, чому ви отримуєте цей результат. Якщо someValueце правда, то ++x, ++yі --yотримати страчені, який неефективне змінити , yале додає до x.

Якщо someValueпомилково, то --xі --yвиконуються, декрементуючи їх обох по одному.


42

Чому компілятор C ++ генерує код, який для істинної гілки потрійного оператора збільшується лише з кроком x

Ви неправильно трактували те, що сталося. Приріст істинної гілки як xі y. Однак yзменшується відразу після цього, беззастережно.

Ось як це відбувається: оскільки умовний оператор має більш високий пріоритет, ніж оператор коми в C ++ , компілятор аналізує вираз таким чином:

   (someValue ? ++x, ++y : --x), (--y);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^

Зверніть увагу на "осиротілих" --yпісля коми. Це те, що призводить до декрементації y, яка була спочатку нарощена.

Я навіть пішов так, щоб ставити дужки навколо справжньої гілки так:

someValue ? (++x, ++y) : --x, --y;

Ви були на правильному шляху, але ви поставили в дужку неправильну гілку: ви можете виправити це, скориставшись дужкою іншої гілки, як це:

someValue ? ++x, ++y : (--x, --y);

Демо (друкує 11 11)


5

Ваша проблема полягає в тому, що потрійний вираз насправді не має вищого переваги, ніж кома. Насправді, C ++ не може бути точно описаний просто пріоритетом - і саме взаємодія між потрійним оператором і комою відбувається там, де він руйнується.

a ? b++, c++ : d++

трактується як:

a ? (b++, c++) : d++

(кома поводиться так, ніби має більший пріоритет). З іншої сторони,

a ? b++ : c++, d++

трактується як:

(a ? b++ : c++), d++

і потрійний оператор має вищий пріоритет.


Я думаю, це все ще знаходиться у царині пріоритету, оскільки існує лише один дійсний синтаксичний аналіз для середньої лінії, правда? І все-таки корисний приклад
sudo rm -rf slash

2

Точка, яку не помічають у відповідях (хоча і торкаються коментарів), полягає в тому, що умовний оператор незмінно використовується (призначений дизайном?) В реальному коді як ярлик для призначення одного з двох значень змінній.

Отже, більшим контекстом буде:

whatIreallyWanted = someValue ? ++x, ++y : --x, --y;

Що на її обличчі абсурдно, тож злочини різноманітні:

  • Мова допускає смішні побічні ефекти у дорученні.
  • Укладач не попередив вас, що ви робите химерні речі.
  • Здається, книга зосереджена на "хитромудріших" питаннях. Можна лише сподіватися, що відповідь у спині полягав у тому, що "Те, що це виражає, залежить від дивних випадкових випадків у надуманому прикладі для створення побічних ефектів, яких ніхто не очікує. Ніколи цього не роби".

1
Призначення однієї з двох змінних - звичайний випадок потрійного оператора, але бувають випадки, коли корисно мати форму вираження if(наприклад, вираз інкременту в циклі for). Більш широкий контекст цілком може бути for (x = 0, y=0; x+y < 100; someValue?(++x, ++y) :( --x, --y))з циклом, який може змінюватись xі yнезалежно.
Мартін Боннер підтримує Моніку

@MartinBonner Я не переконаний, і цей приклад, здається, дуже добре робить точку бо-Персон, як він цитував Тоні Хоара.
Тарин
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.