C Operator Precedence

Given the statement below, what is pf?

  1. a pointer to an array of 10 arrays of 5 pointers to functions, with float and double parameters, that return an int
  2. an array of 10 arrays of 5 pointers to pointers to functions, with float and double parameters, that return an int
  3. a pointer to a pointer to an array of 10 arrays of 5 functions, with float and double parameters, that return an int
  4. an array of 10 arrays of pointers to an array of 5 pointers to functions, with float and double parameters, that return an int

I posted this question in Facebook and the nerdy programmers started replying. This question is actually about the Operator Precedence in C. The normal solution is to first look for an operator precedence chart and start analyzing. Here’s a chart from cppreference.com

C Operator Precedence Chart

Just like PEMDAS in Math, we start with the innermost parenthesis and solve outwards. In the statement above, we have 3 sets of parentheses.

int (*(*pf)[10][5])(float, double);

The red parenthesis is the innermost and thus we start there. Let’s isolate that.

*pf

Here, we can see that pf is a pointer. To what, we don’t know yet. But we do know it’s a pointer. Therefore, we say that pf is a pointer to

Going outwards, we encounter the following expression.

*(*pf)[10][5]

We have 2 index operators on the right and 1 dereference operator on the left. According to our chart, Array subscripting (index operator) comes before Indirection (dereference). The next question is, which comes first, the 10 or the 5? According to the chart, array subscripting is left-to-right. Therefore, 10 comes before 5. Let’s make that clearer with some colors.

*(*pf)[10][5]

Therefore we continue what pf is: pf is a pointer to an array of 10 arrays of 5 pointers to… well, we don’t know yet. We can already answer the question above but let’s continue on to see what pf really is.

int (*(*pf)[10][5])(float, double);

Next, we see a function call on the right and a data type on the left. The function call is really high in the chart and the data type is not an operator, therefore, it’s not in the chart. But it doesn’t really matter because in declarations, data type is always last.

int (*(*pf)[10][5])(float, double);

Let’s continue; pf is a pointer to an array of 10 arrays of 5 pointers to a function, with float and double parameters, returning an int. And there we have the answer.

Looking back at the first step, what if the innermost parenthesis is absent? Which of the 2 parenthesis do we consider first?

The parenthesis acts as a function call given the following 2 requirements:

  • the left of it is a function name or a pointer to a function
  • inside are comma-separated data types known as the parameter list, which can be empty

Otherwise, the parenthesis are used to group expressions.

int (**pf[10][5])(float, double);

The first set can’t be a function call since its left is a data type. The second could be a function call because on its left is an expression. We’ll know if this is a legal statement if the first set is a pointer to a function (we can immediately deduce that it’s not a function name).

In this scenario, what is pf? Let’s follow the operator precedence again starting with the innermost group.

**pf[10][5]

We already know that the index operators have higher priority compared to dereference operators. We can also see from the chart that index operators work from left-to-right while dereference operators work from right-to-left. Therefore, we say that pf is an array of 10 arrays of 5 pointers to pointers to… something.

int (**pf[10][5])(float, double);

We continue outwards and encounter the same scenario from the first question. Therefore, we say that pf is an array of 10 arrays of 5 pointers to pointers to a function, with float and double parameters, which return an int.

Note that this could even get more complicated with the function parameters being as complex, like the following:

Again, this is a legit statement. Here, pf is an array of 2 arrays of 3 pointers to functions, with an array of 4 pointers to functions (with double as parameter) returning a float as parameter, that returns an int. The difficulty with this is that parameter names can be ignored during declarations (prototypes) in C/C++. Meaning, the following statement is equivalent to the previous one.

Now that’s kinda confusing. Complicated? Well, there’s an easier way. Introducing, the Right-Left method of reading C code. This is basically how the compiler interprets it as well.

The Right-Left Method

We know for a fact that parenthesis, used to group expressions, would always be highest priority. Given multiple parenthesis, we start with the innermost. After the parenthesis, the unary operators occurring at the right of expressions have higher priority than unary operators occurring at the left of expressions. After which, we get binary operators. Again, those operators that occur on the right have higher priority on the ones that occur on the left. Meaning, we only need to remember 3 rules:

  1. Grouping parenthesis are always highest, always start with innermost
  2. Unary Operators > Binary Operators
  3. Operators on the Right > Operators on the Left

Do note that binary operators occurring on the right (arithmetic, logic, bitwise) have different priorities which we don’t have a technique for. For that, I use the operator precedence chart.

Let’s discuss the right-left method. Given the first problem, we start with the identifier in the innermost parenthesis

pf)

Looking at the right of pf, we see a closing parenthesis. This means, we go to the left.

*pf)

On the left, we see a dereference operator. So we say, pf is a pointer to. We continue to the left.

(*pf)

We encounter an opening parenthesis, which means, go to the right

(*pf)[10]

We encounter an index operator. We append array of 10 to our definition. We continue to the right

(*pf)[10][5]

We encounter another index operator. We append array of 5 to our definition. We continue to the right.

(*pf)[10][5])

We encounter a closing parenthesis. We change direction to the left.

*(*pf)[10][5])

We see a dereference operator. We append pointer to to our definition. We continue to the left.

(*(*pf)[10][5])

We encounter an opening parenthesis. This means, we go to the right.

(*(*pf)[10][5])(

We see an opening parenthesis while we’re going to the right. This means, it’s a start of a function call. We append a function to our definition and continue to the right

(*(*pf)[10][5])(float

We encounter a data type and since we’re in a function call, this is a parameter of the function. We append with a float parameter to our definition. We continue to the right.

(*(*pf)[10][5])(float, double

We encounter another data type and since we’re still in a function call, this is a parameter of the function. Similarly, we append with a double parameter to our definition. We continue to the right.

(*(*pf)[10][5])(float, double)

We encounter a closing parenthesis which indicates the end of the function call. This also means we go to the left. The closing of a function appends that returns to our definition. We now go to the left.

int (*(*pf)[10][5])(float, double)

We encounter a data type and simply append that, int, to our definition.

Combining all those, and correcting the errors in english, we get pf is a pointer to an array of 10 arrays of 5 pointers to functions, with float and double parameters, that return an int. Now wasn’t that exponentially easier.

As an added rule, not shown in the example, function could only be preceded by an identifier or a pointer. Meaning, the following statements are invalid because C does not allow arrays of functions.

However, the following statements are allowed.

The Right-Left method is also applicable to other statements other than declaration. Just remember the 3 rules above and have the operator precedence chart when you encounter left-to-right binary operators.

What’s the point of such complexity? It’s for having callback functions in C.