Programming languages have a lot of operators. More than most calculators anyway. Do you know what they are? Did you know there’s an order to them? This episode explains precedence, associativity, and evaluation order.
Let’s say you have this statement:
int i = 2 + 3 * 4 – 5;
The multiplication takes precedence over the addition and subtraction and goes first. The multiplication operator uses the symbol * and is a binary operator. This means it takes two operands. In this case, that is 3 and 4. After multiplying 3 by 4, it’s as if the statement now becomes:
int i = 2 + 12 – 5;
Both the addition and subtraction operators have the same precedence but they have left-to-right associativity which means that the addition is performed next. And then the subtraction.
If you want to change this order, you can use parenthesis. So if you wanted 2 to be added with 3 first, then you could write your code like this:
int i = (2 + 3) * 4 – 5;
You also have unary operators that require only a single operand like this pointer dereference:
int i = 5;
int * iPtr = &i;
int iCopy = *iPtr;
Notice how the same asterisk symbol is used to mean multiplication above, then used here to declare a pointer type and then used again to dereference the iPtr pointer and assign the value 5 to the iCopy variable. Many languages use symbols for different purposes and they really mean different things. The meaning will be clear form the usage. It has to be or the compiler won’t be able to understand your code.
You can also write the first example to use methods instead of integer values. Then it’ll look like this:
int i = methodA() + methodB() * methodC() – methodD();
Here, I’ve just replaced 2, 3, 4, and 5 with method calls. You already know that the multiplication will be done first before the addition or subtraction. But what order will the methods themselves be called? In other words, will the compiler choose to call methodB before or after calling methodC? It has to call both methods before it can perform the multiplication and that’s all you can rely on. The actual order is up to the compiler implementation and could change from one version of your compiler to the next.
Listen to the full episode for more about operators, or you can read the full transcript below.
Let’s say you have a local integer variable that you want to assign a value like this:
◦ int i = 2 + 3 * 4 – 5;
This is called a compound expression because there’s more than one operator involved. First there’s the assignment operator. Now, I don’t count this operator when classifying something as a compound expression or not. At least that makes sense to me. But I could be wrong about that. The important thing I wanted to explain is the part after the assignment operator.
I’m going to take a short detour here to explain operators and how many operands they need. Then I’ll come back to the compound expression.
In C++ and in many other languages, you have unary, binary, and ternary operators. A unary operator takes a single operand. The pointer dereference operator is a good example of a unary operator. It looks just like the asterisk used in multiplication but when applied to a pointer, it means to take the value that the pointer points to. It just needs a single pointer variable to work.
A binary operator requires two operands. The addition, multiplication, and subtraction operators, plus, asterisk, and minus, are all binary because they need two values to work. You can’t multiply just a single number.
Notice how the asterisk symbol is used for both dereferencing pointers and for multiplying numbers. This happens sometimes and the meaning of the symbol needs to be understood from how it’s being used.
There’s a single ternary operator that needs three operands. This is the conditional operator and uses two symbols between the three operands. It expects the first operand which will be tested for true or false, followed by a question mark symbol, then an operand that will be returned if the first test was true. Then it uses a colon symbol before the third operand. The third operand is what will be returned if the first test was false. You use this operator when you want to quickly select one of two values based on whether or not another value is true or false.
And finally, there’s a function call operator that can take any number of operands. This operator uses the parenthesis with the method name before and outside of the parenthesis and all of the parameters that will be passed to the method inside the parenthesis. Each parameter is separated with a comma.
So in total, you have operators that require one, two, three, or an unlimited number of operands.
Okay, so back to the statement. You have code that assigns a value to an integer called i like this:
◦ int i = 2 + 3 * 4 – 5;
What value will be assigned to i? If you just evaluate everything from left to right, you get 2 plus 3 which is 5, then multiplied by 4 gives 20, then subtracting 5 gives 15.
But that’s not what happens. the reason is that the multiplication has a higher precedence than addition or subtraction.
So when you want to evaluate 2 + 3 * 4 – 5, you need to multiple 3 times 4 to get 12. You can then think of the expression as 2 + 12 – 5. The value 12 is more than just a convenient way to think about this. The compiler will create a temporary variable to hold the value 12. Actually, since all of the values involved, 2, 3, 4, and 5, are constants known at compile time, then the compiler will most likely perform the calculation at compile time and just replace everything with an assignment of the final value.
As it turns out, both addition and subtraction have the same precedence level. They also have an associativity of left-to-right so that means the addition will be performed first followed by subtraction. So 2 plus 12 is 14 and then subtracting 5 gives 9. And the value 9 gets assigned to the local integer variable i.
You can control the precedence by putting parenthesis around parts that you want to perform first. This works because parenthesis is just another operator with a very high precedence. Let’s say that you wanted to make sure that the 2 gets added to the 3 first. Put parenthesis around the portion of the expression that says 2 + 3. This will give you 5 which will then get multiplied by 4 which results in 20. And then the expression becomes 20 – 5. The final answer is 15.
You don’t have to just use parenthesis to change the precedence. Sometimes it makes your code easier to read if you put parenthesis around parts that would have been had the same precedence without the parenthesis. Technically, these parenthesis are redundant. But they can add value especially to us human programmers by stating explicitly what we want to happen.
I’ll explain what happens if you have methods instead of simple integers right after this message from our sponsor.
( Message from Sponsor )
As a special bonus for joining Take Up Code like the sponsor message just described, I’ll make sure that you get an email with all the C++ operators in an easy to read chart that you can refer to anytime you need. This chart will be ready within a few days after this podcast episode is released.
Instead of simple numbers what if the earlier example called methods instead. What I mean is if you have code that says:
◦ int i = methodA() + methodB() * methodC() – methodD(), then what happens? This is almost the same as int i = 2 + 3 * 4 – 5; All I did was replace the integers with calls to methods A, B, C, and D.
You already know that because of operator precedence, this code will multiply the result of methodB with the result of calling methodC. This creates a temporary variable just like before. You don’t have access to this temporary variable by name but it will exist. Because these are methods that can return any value at runtime, the compiler can’t optimize them away like it can with constant integer literals 2, 3, 4, and 5.
The addition and subtraction will also be done just like before. The only difference is that the methods have to be called first. This makes sense because how can you add something when you don’t yet know what to add? You know that the methods will be called before performing the multiplication, addition, and subtraction.
But what about the order that the methods themselves will be called? This is called the evaluation order and is up to the compiler writers to decide what’s best. That means you can’t rely on any particular evaluation order that the methods will be called. You have the same issue when calling a method with the method call operator. If you’re calling other methods to get the values to be passed as parameters to a method, then you can’t rely on any particular order that each operand will be evaluated. All you know is that all the parameters will be evaluated before the method will be called.
This is important because you need to be careful that you don’t write the methods so they affect one another. If methodA changes a member variable or a global variable that methodB also uses, then will methodB get the previous value or will it get the value after methodA is done? You don’t know. And writing code where you don’t know the result is bad.
Maybe it’ll work just fine initially. And your program will continue to grow as you add more and more features. Maybe a year or more will go by and this piece of code will be long forgotten. Then one day, you need to upgrade your compiler to a new version and don’t have time to run through all of your tests so you perform some basic tests and when everything looks good, you release the new version of your program to your customers. Within a couple days, you start hearing reports about strange behavior and it doesn’t make sense. Why would part of your program suddenly break that’s been working just fine? Because the new compiler decided it was better to call methodA and methodB in a different order.