Подтвердить что ты не робот

Почему '- ++ a- - ++ + b--' оценивается в этом порядке?

Почему следующая печать bD aD aB aA aC aU вместо aD aB aA aC bD aU? Другими словами, почему b-- оценивается до --++a--++?

#include <iostream>
using namespace std;

class A {
    char c_;
public:
    A(char c) : c_(c) {}
    A& operator++() {
        cout << c_ << "A ";
        return *this;
    }
    A& operator++(int) {
        cout << c_ << "B ";
        return *this;
    }
    A& operator--() {
        cout << c_ << "C ";
        return *this;
    }
    A& operator--(int) {
        cout << c_ << "D ";
        return *this;
    }
    void operator+(A& b) {
        cout << c_ << "U ";
    }
};

int main()
{
    A a('a'), b('b');
    --++a-- ++ +b--;  // the culprit
}

Из того, что я собираю, вот как выражение анализируется компилятором:

  • Маркировка препроцессора: -- ++ a -- ++ + b --;
  • Приоритет оператора 1: (--(++((a--)++))) + (b--);
  • + является ассоциацией слева направо, но тем не менее компилятор может сначала оценить выражение справа (b--).

Я предполагаю, что компилятор решил сделать это так, потому что это приводит к улучшению оптимизированного кода (меньше инструкций). Однако стоит отметить, что я получаю тот же результат при компиляции с /Od (MSVC) и -O0 (GCC). Это подводит меня к моему вопросу:

Поскольку меня попросили об этом на тесте, который в принципе должен быть реализацией/компилятором-агностиком, есть ли что-то в стандарте С++, который предписывает вышеуказанное поведение, или это действительно неуказано? Может кто-то цитата из выдержки из стандарта, которая подтверждает или? Неправильно ли было иметь такой вопрос в тесте?

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

4b9b3361

Ответ 1

Оператор выражения

--++a-- ++ +b--;  // the culprit

можно представить следующим образом:

сначала, как

( --++a-- ++ )  + ( b-- );

тогда как

( -- ( ++ ( ( a-- ) ++ ) ) )  + ( b-- );

и, наконец, как

a.operator --( 0 ).operator ++( 0 ).operator ++().operator --().operator  + ( b.operator --( 0 ) );

Вот демонстративная программа.

#include <iostream>
using namespace std;

#include <iostream>
using namespace std;

class A {
    char c_;
public:
    A(char c) : c_(c) {}
    A& operator++() {
        cout << c_ << "A ";
        return *this;
    }
    A& operator++(int) {
        cout << c_ << "B ";
        return *this;
    }
    A& operator--() {
        cout << c_ << "C ";
        return *this;
    }
    A& operator--(int) {
        cout << c_ << "D ";
        return *this;
    }
    void operator+(A& b) {
        cout << c_ << "U ";
    }
};

int main()
{
    A a('a'), b('b');
    --++a-- ++ +b--;  // the culprit

    std::cout << std::endl;

    a.operator --( 0 ).operator ++( 0 ).operator ++().operator --().operator  + ( b.operator --( 0 ) );

    return 0;
}

Его вывод

bD aD aB aA aC aU 
bD aD aB aA aC aU 

Вы можете представить последнее выражение, записанное в функциональной форме, как postfix выражение формы

postfix-expression ( expression-list ) 

где выражение postfix

a.operator --( 0 ).operator ++( 0 ).operator ++().operator --().operator  +

и список выражений

b.operator --( 0 )

В стандарте С++ (вызов функции 5.2.2) говорится, что

8 [Примечание: оценки постфиксного выражения и аргументов все они не зависят друг от друга. Все побочные эффекты оценки аргументов секвенированы до ввода функции (см. 1,9). -end note]

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

Ответ 2

Я бы сказал, что они ошибались, чтобы включить такой вопрос.

За исключением отмеченных, следующие выдержки из статьи [intro.execution] из N4618 (и я не думаю, что любой из этих вещей изменился в более поздних черновиках).

В пункте 16 представлено основное определение sequenced before, indeterminately sequenced и т.д.

В пункте 18 говорится:

За исключением тех случаев, когда отмечено, оценки операндов отдельных операторов и подвыражений отдельных выражений не подвержены.

В этом случае вы (косвенно) вызываете некоторые функции. Правила там также довольно просты:

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

Поместите это в пулевые точки для более прямого указания порядка:

  • сначала оцените аргументы функции, а все, что назначает вызываемую функцию.
  • Вычислить тело самой функции.
  • Оцените другое (суб) выражение.

Никакое перемежение не допускается, если только что-то не запускает поток, чтобы позволить что-то еще выполнять параллельно.

Итак, происходит ли какое-либо из этих изменений до того, как мы вызываем функции через перегрузки оператора, а не напрямую? В пункте 19 говорится: "Нет":

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

§ [expr]/2 также говорит:

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

Индивидуальные операторы

Единственный оператор, который вы использовали, который имеет несколько необычные требования в отношении последовательности, - это пост-инкремент и пост-декремент. Они говорят (§ [expr.post.incr]/1:

Вычисление значения выражения ++ секвенируется перед модификацией объекта операнда. Что касается вызова функции с неопределенной последовательностью, то операция postfix ++ является отдельной оценкой. [Примечание. Поэтому вызов функции не должен вмешиваться между преобразованием lvalue-to-rvalue и побочным эффектом, связанным с любым одним оператором postfix ++. -end note]

В конце, однако, это в значительной степени то, что вы, вероятно, ожидаете: если вы передадите x++ в качестве параметра функции, функция получит предыдущее значение x, но если x также входит в объем внутри функции, x будет иметь увеличенное значение к тому времени, когда тело функции начнет выполняться.

Однако оператор + не задает порядок оценки своих операндов.

Резюме

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

Более конкретно, в этом случае b-- является операндом вызова функции, а --++a-- ++ является выражением, которое обозначает вызываемую функцию (или, по крайней мере, объект, на который будет называться функция - -- обозначает функцию внутри этого объекта). Как отмечено, порядок между этими двумя не указан (и operator + не указывает порядок оценки его левого и правого операндов).

Ответ 3

В стандарте С++ нет ничего, что говорит, что вещи должны оцениваться таким образом. С++ имеет концепцию sequenced-before, где некоторые операции гарантированно будут выполняться до других операций. Это частично упорядоченное множество; то есть операции sosome секвенируются перед другими, две операции не могут быть секвенированы до другого eath, а если a секвенирован до b, а b секвенирован до c, то a секвенируется до c. Тем не менее, существует много типов операций, которые не имеют гарантированных последовательностей. До С++ 11 вместо этого существовала концепция точки последовательности, которая не совсем такая же, но схожая.

Очень немногие операторы (только ,, &&, ?: и ||, я считаю) гарантируют точку последовательности между их аргументами (и даже тогда, до С++ 17, эта гарантия не существует когда операторы перегружены). В частности, дополнение не гарантирует такой вещи. Компилятор может сначала оценить левую сторону, сначала оценить правую часть, или (я думаю), даже оценить их одновременно.

Иногда изменение параметров оптимизации может изменить результаты или сменить компиляторы. Видимо, вы не видите этого; здесь нет гарантий.

Ответ 4

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

Также обратите внимание, что в вашем случае двоичный + реализуется как функция-член, что создает некоторую поверхностную асимметрию между его аргументами: один аргумент является "регулярным" аргументом, другой - this. Возможно, некоторые компиляторы "предпочитают" сначала оценивать "обычные" аргументы, что приводит к тому, что сначала оценивается b-- в ваших тестах (вы можете получить другой порядок от одного и того же компилятора, если вы реализуете свой двоичный + как автономная функция). Или, может быть, это вообще не имеет значения.

Clang, например, начинается с оценки первого операнда, оставляя b-- для более позднего.

Ответ 5

Возьмите в приоритете приоритета операторов в С++:

  • a ++ a - Приращение суффикса/постфикса и декремент. Слева направо
  • ++ a - a Приращение и уменьшение префиксов. Справа налево
  • a + b a-b Сложение и вычитание. Слева направо

Сохраняя список в уме, вы можете легко прочитать выражение даже без круглых скобок:

--++a--+++b--;//will follow with
--++a+++b--;//and so on
--++a+b--;
--++a+b;
--a+b;
a+b;

И не забывайте о существенных префиксах разницы и постфиксных операциях в терминах оценки порядка переменных и выражения))