Search code examples
c++operator-overloadingcompiler-optimizationevaluationoperator-precedence

Why is '--++a-​- ++ +b--' evaluated in this order?


Why does the following print bD aD aB aA aC aU instead of aD aB aA aC bD aU? In other words, why is b-- evaluated before --++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
}

From what I gather, here's how the expression is parsed by the compiler:

  • Preprocessor tokenization: -- ++ a -- ++ + b --;
  • Operator precedence1: (--(++((a--)++))) + (b--);
  • + is left-to-right associative, but nonetheless the compiler may choose to evaluate the expression on the right (b--) first.

I'm assuming the compiler chooses to do it this way because it leads to better optimized code (less instructions). However, it's worth noting that I get the same result when compiling with /Od (MSVC) and -O0 (GCC). This brings me to my question:

Since I was asked this on a test which should in principle be implementation/compiler-agnostic, is there something in the C++ standard that prescribes the above behavior, or is it truly unspecified? Can someone quote an excerpt from the standard which confirms either? Was it wrong to have such a question on the test?

1 I realize the compiler doesn't really know about operator precedence or associativity, rather it cares only about the language grammar, but this should get the point across either way.


Solution

  • The expression statement

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

    can be represented the following way

    at first like

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

    then like

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

    and at last like

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

    Here is a demonstrative program.

    #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;
    }
    

    Its output is

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

    You can imagine the last expression written in the functional form like a postfix expression of the form

    postfix-expression ( expression-list ) 
    

    where the postfix expression is

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

    and the expression-list is

    b.operator --( 0 )
    

    In the C++ Standard (5.2.2 Function call) there is said that

    8 [Note: The evaluations of the postfix expression and of the arguments are all unsequenced relative to one another. All side effects of argument evaluations are sequenced before the function is entered (see 1.9). —end note]

    So it is implementation-defined whether at first the argument will be evaluated or the postfix expression. According to the showed output the compiler at first evaluates the argument and only then the postfix expression.