Can anyone show an example where this precedence matters?

Viewed 303

I'm reading precedence and associativity. In the table, I observed these two things -

(i) precedence of postfix increment(or decrement) operator is greater than precedence of prefix increment(or dec.) operator.

(ii) associativity of postfix inc.(or dec.) operator is left-to-right but that of prefix increment(or dec.) operator is right-to-left.

I'm not sure why it is needed. Can anyone help me by showing code(separately for each case), which shows the need of these two facts? Thanks.

I tried to think about cases but not getting any such(as I'm very new to programming).

5 Answers

This is needed for an expression like

data = *pointer++;

You to obtain the value at pointer in data, and then increment it to the next element. If the precedence of the postfix weren't greater, you'd end up with an incremented value in data.

And the associativity of the prefix operator is right-to-left because you want an expression like

data = **pointer_to_pointer;

to be evaluated from right to left as if you'd write

data = *(*pointer_to_pointer);

In C, as in most languages, postfix operators bind more tightly than prefix operators, and prefix operators bind more tightly than binary operators. (There are exceptions to the second part, but not in C.) That generally corresponds to intuitions about the meaning of expressions.

For example, almost everyone would expect

-a[0]

to mean "the negative of element 0 of the array a", rather than "element 0 of the array -a", particularly in languages like C where "the array -a" is not meaningful. Similarly,

-a++

has the expected meaning "the negative of the current value of a, which is subsequently incremented." Again, incrementing -a is meaningless since -a is not a variable.

Naive intuitions might not work as well with more obscure operators, so it is useful to maintain consistency. While it is imaginable that there might exist a prefix operator which almost always needs to be surrounded in parentheses because it doesn't bind tightly enough, making that operator an exception to the general rule us likely to create more surprises than it solves, and few languages take this path.

So prefix and postfix uses of ++ and -- have syntax defined by this common rule. Nonetheless, in C at least, it is an error to apply both to the same operand (++a--) because the value returned, both by the pre- and post-fix versions, is not an lvalue, while the operand is required to be an lvalue. In that sense, the particular case of comparing precedences of prefix and postfix ++ and -- never shows up in a correct program. But other combinations of prefix and postfix operators do, and precedence levels need to apply homogenously.

There is another sense in which the precedence and associativity declarations are redundant. It is syntactically meaningless to talk about the associaticity between two prefix operators, or between two postfix operators. And if prefix and postfix operators have different precedence levels, it is also meaningless to talk about associativity between a prefix and a postfix operator. So the associativity is irrelevant.

However, you could stretch the concept of associativity and say that all unary operators have the same precedence and all of them associate to the right. That will actually produce a correct parser, and was used in the definition of B (C's predecessor). But it really is too confusing for most people not accustomed to grammatical analysis.

(i) precedence of postfix operator is greater than precedence of prefix operator.

Consider the code:

int x[] = {1,2,3};
int *p = x;
int y;
y = ++p[0]; 

This will increment the first element of x and assign it to y.

versus this one, where we explicitly changing the precedence so the prefix ++ gets a higher one:

y = (++p)[0];

This will not increment x elements but will move p to the second element of x and assign y with it.

(ii) associativity of postfix operator is left-to-right but that of prefix operator is right-to-left.

This means that: p->x->y should be read as (p->x)->y, and not p->(x->y) which does not even make any sense.

Same for other the other operators of the postfix group - the RTL associativity just doesn't make sense for them: p[x][y][z] is the same as ((p[x])[y])[z], but p[x]([y]([z])) is meaningless (and illegal).

From historical point of view, the precedence of the operators was influenced by the B and BCPL programming languages. In the article The Development of the C Language, Dennis Ritchie explains how he chose the precedence.

An accident of syntax contributed to the perceived complexity of the language. The indirection operator, spelled * in C, is syntactically a unary prefix operator, just as in BCPL and B. This works well in simple expressions, but in more complex cases, parentheses are required to direct the parsing. For example, to distinguish indirection through the value returned by a function from calling a function designated by a pointer, one writes *fp() and (*pf)() respectively

There is no clear logic about the precedence -- you need to memorize it or to use parenthesis when you are not sure about the precedence.

The precedence and associativity of the operators fall out of the language grammar. For postfix and unary operators, that's as follows:

postfix-expression:
    primary-expression
    postfix-expression [ expression ]
    postfix-expression ( argument-expression-listopt )
    postfix-expression . identifier
    postfix-expression -> identifier
    postfix-expression ++
    postfix-expression --
    ( type-name ) { initializer-list }
    ( type-name ) { initializer-list , }

unary-expression:
    postfix-expression
    ++ unary-expression
    -- unary-expression
    unary-operator cast-expression
    sizeof unary-expression
    sizeof ( type-name )
    _Alignof ( type-name )

unary-operator: one of
    & * + - ~ !

cast-expression:
    unary-expression
    ( type-name ) cast-expression

C 2011 Online Draft, Appendix A.2 Phase Structure Grammar

So, how does that determine precedence and associativity, and why does it matter? Let's start with an expression like *p++ - are we dereferencing p++, or are incrementing *p? Those are two very different operations, so it matters how the grammar is structured. Let's trace through it:

        *            p            ++
        |            |            |
        |         primary         |
        |        expression       |
        |            |            |
        |         postfix         |
        |        expression       |
        |            |            |
        |            +------+-----+
        |                   |
        |                postfix
        |               expression
        |                   |
        |                 unary
        |               expression
        |                   |
      unary                cast
     operator           expression
        |                   |
        +---------+---------+
                  |
                unary
              expression

In English:

  1. unary-expression produces unary-operator cast-expression
  2. unary-operator produces *
  3. cast-expression produces unary-expression
  4. unary-expression produces postfix-expression
  5. postfix-expression produces postfix-expression ++
  6. postfix-expression produces primary-expression
  7. primary-expression produces p

This means the expression *p++ is parsed as *(p++) - the * operator will be applied to the result of p++.

Same kind of thing for *p[i] - we will wind up dereferencing the pointer at p[i], rather than subscripting *p.

For a slightly more complicated example that gets into associativity, let's look at the member selection operator ->, as in the expression foo->bar->bletch->blurga. The grammar for the -> member selection operator is

postfix-expression -> identifier

This tells us that foo->bar->bletch reduces to postfix-expression and blurga reduces to identifier. Hence, the associativity of the operator is right-to-left, and the expression parses as ((foo->bar)->blurga)->bletch, rather than foo->(bar->(blurga->bletch)).

Those tables you're looking at are summaries of how the grammar is set up. The grammar is set up the way it is so that operator and operand groupings are somewhat intuitive. You expect an expression like ++foo.bar[i] to increment foo.bar[i], you expect *f() to dereference the pointer value returned from a function, etc.

Related