Associativity of function call operator in C
Asked Answered
H

3

31

I was going through the topic of associativity of C operators.

There I came across this fact that the function call operator () has a left to right associativity. But associativity only comes to play when multiple operators of the same precedence occur in an expression. But I couldn't find any example involving function call operator where associativity plays a crucial role.

For example in the statement a = f(x) + g(x);, the result depends on the evaluation order and not on the associativity of the two function calls. Similarly the call f(g(x)) will evaluate function g() first and then the function f(). Here we have a nested function call and again associativity doesn't play any role.

The other C operators in this priority group are array subscript [], postfix ++ and postfix --. But I couldn't find any examples involving a combination of these operators with () where associativity plays a part in expression evaluation.

So my question is does the associativity of function call being defined as left to right affect any expression in C? Can anyone provide an example where the associativity of function call operator () does matter in expression evaluation?

Haemolysis answered 10/8, 2015 at 11:1 Comment(4)
What if f is an expression itself (like one function pointer selected from an array)?Fukuoka
Any function that returns a pointer to a function can be used as an example, because function pointer dereferencing is not necessary with the function call operator. So you start having things like f()()()() and it's valid C. See Grzegorz's answer for a possible example.Fragmentary
28 upvotes.... It kind of has to be left to right. Would you want to live in a world where f(a)(b) was a call to f passing b then calling the result with a (that is to say (f(b))(a)) - would you?Mcvey
I disagree with the existing answers, magnificently voted or not. They do explain some basics well, but this answer goes way deeper. And yes, we could have a long argument about C grammar distinguishing "primary" and "unary" operators, but that distinction is just an artifact of the grammar as well.Hazel
Q
45

Here is an example, where left-right associativity of function call operator matters:

#include <stdio.h>

void foo(void)
{
    puts("foo");
}

void (*bar(void))(void) // bar is a function that returns a pointer to a function
{
    puts("bar");
    return foo;
}

int main(void)
{
    bar()();

    return 0;
}

The function call:

bar()();

is equivalent to:

(bar())();
Quijano answered 10/8, 2015 at 11:10 Comment(9)
Brilliant. Thank you so much.Haemolysis
In what sense is associativity relevant when dealing with unary operators that are all prefix or all postfix? I would think it would be mainly relevant in cases where prefix and postfix operators are both applied to the same item, as in *foo(). I can't think of any definition for foo that would allow both (*foo)() and *(foo()) to be legal in C (such a thing could exist in C++), but the fact that each form is legal in some situations and only the latter can be replaced with *foo() would demonstrate the relative associativity of * and ().Functionalism
@supercat: Here, it tells how to "bind" with its operand. Though I agree, that is mainly relevant with other operators (of the same precedence), note that () operator has higher precedence than * (dereference), so even if such a example does exit, it will not prove anything. The candidates are ++, -- (both prefix), . and ->, but no example could be found (at least not in C), as a) function cannot return a lvalue, b) second operand for . and -> must be an identifier.Quijano
@GrzegorzSzpetkowski: Syntactically, I don't think the . and -> tokens are really binary operators; instead, I would regard the combination of one of those tokens along with the succeeding token to behave together as a postfix operator. I know C++ doesn't handle the -> operator that way, but the way that the . and -> tokens bind to the next token is unlike anything else any other operators do.Functionalism
@supercat: I would say that . and -> share characterictics between postfix expression and binary operator. The C11 §6.5.2.3/p1 says "and the second operand shall name a member" about both, thus they have two operands and from that point it seems reasonable to consider them as binary. On the other hand, they are parsed in "postfix manner", as you wrote.Quijano
@GrzegorzSzpetkowski: To my mind, if they were really operators, it would be possible to write something like foo -> (expr ? bar: boz) and have its meaning be equivalent to expr ? foo->bar : foo->boz (actually, come to think of it having C allow that could make some kinds of code more legible, and it wouldn't make any currently-legal constructs ambiguous; it would, however, grossly violate the "no new invention" rule). As it is, though, I think it's more useful to say that binary operators connect expressions, and thus . and -> aren't binary operators, than to regard those things...Functionalism
@supercat: -> and . are operators but they only have one expression operand, on the left. The RHS is an identifier but not an expression. As such they're effectively postfix unary operators, not binary operators.Billiton
@R..: Yes and no. They are "binary" in common sense of english word, since they consumes two operands (no matter which form) and I have given above an excerpt from C standard to cover this. But at the same time I agree with you both. They are effectively postfix unary operators, as second operand cannot be exanded into an expression.Quijano
This is not an example where the associativity of the function call operator matters. There is just no other way that bar()() could possibly be interpreted than as (bar())(); notably bar(()()) is no option, not just because now the argument of bar is not syntactic, but more fundamentally because parentheses that were added now become the "function call operator" themselves, and cannot be omitted. So right(-to-left) associativity would just not make any sense. Nor would it for other postfix operators, which left-associate; similarly prefix operators are necessarily right-associative.Dariusdarjeeling
A
15

In addition to @GrzegorzSzpetkowski's answer, you can also have the following:

void foo(void) { }

int main(void) {
    void (*p[1])(void);
    p[0] = foo;
    p[0]();
    return 0;
}

This creates an array of function pointers, so you can use the array subscript operator with the function call operator.

Arrestment answered 10/8, 2015 at 11:30 Comment(2)
Nice Answer. Thank you.Haemolysis
And if those functions in the arrays would be int* (*p[1])(int) instead, you could have p[0](1)[2];. Take the first function pointer, call it with argument 1, and take the third array element returned. There's almost no bound to the complexity of C expressionsBenefaction
D
3

The statement that function application associates to the left is entirely redundant in the C language definition. This "associativity" is implied by the fact that a function argument list appears right of the function expression itself and must be enclosed in parentheses. This means that for a given function expression, there can never be any doubt about the extent of its argument list: it extends from the obligatory opening parenthesis after the function expression to the matching closing parenthesis.

The function expression (which often is just an identifier, but may be more complicated) might itself be a (bare) function call, as in f(n)(1,0). (Of course this requires the value returned by f(n) to be something that can then be called with argument list (1,0), for which C has some possibilities and C++ a lot more, but these are semantic considerations that only come to play after parsing, so they should be ignored for the associativity discussion.) This means the syntax rule for function calls is left-recursive (the function part can itself be a function call); this may be formulated by saying "function calls associate to the left", but the fact is obvious. By contrast the right part (argument list) cannot be a (bare) function call because of the required parentheses, so there cannot be right recursion.

For instance consider (a)(b)(c) (where I put in redundant parenthesis in order to suggest symmetry and possible ambiguity). Here in itself (b)(c) might be taken to be the call of b (redundantly parenthesised) with argument c; however such a call cannot be construed to be the argument of a, since that would require additional parentheses as in (a)((b)(c)). So without any mention of associativity, it is clear that (a)(b)(c) can only mean that a is called with argument b, and the resulting value is called with argument c.

Dariusdarjeeling answered 11/8, 2015 at 5:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.