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

17

Почему следующая печать 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). Это подводит меня к моему вопросу:

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

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

    
задан Konstantin 06.04.2017 в 20:56
источник

5 ответов

18

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

--++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 )

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

  

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

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

    
ответ дан Vlad from Moscow 06.04.2017 в 21:31
источник
14

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

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

Параграф 16 содержит базовое определение sequenced before , indeterminately sequenced и т. д.

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

  

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

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

  

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

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

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

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

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

  

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

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

  

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

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

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

  

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

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

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

Резюме

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

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

ответ дан Jerry Coffin 06.04.2017 в 21:32
источник
7

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

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

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

    
ответ дан Daniel H 06.04.2017 в 21:10
источник
4

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

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

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

    
ответ дан AnT 06.04.2017 в 21:27
источник
-1

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

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

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

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

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

    
ответ дан Andrey Rybak 07.04.2017 в 12:49
источник