Operator precedence and associativity

Chapter introduction

This chapter builds on concepts from the Introduction to literals and operators lesson. A quick review follows:

An operation is a mathematical process involving zero or more input values (called operands) that produces a new value (called an output value). The specific operation to be performed is denoted by a construct (typically a symbol or pair of symbols) called an operator.

For example, as children we all learn that 10 + 5 equals 15. In this case, the literals 10 and 5 are the operands, and the symbol + is the operator that tells us to apply mathematical addition on the operands to produce the new value 15. Because there is only one operator being used here, this is straightforward.

In this chapter, we'll discuss topics related to operators and explore many of the common operators that C++ supports.

Evaluation of compound expressions

Now, let's consider a compound expression, such as 6 + 3 * 4. Should this be grouped as (6 + 3) * 4 which evaluates to 36, or 6 + (3 * 4) which evaluates to 18? Using normal mathematical precedence rules (which state that multiplication is resolved before addition), we know that the above expression should be grouped as 6 + (3 * 4) to produce the value 18. But how does the compiler know?

In order to evaluate an expression, the compiler must do two things:

  • At compile time, the compiler must parse the expression and determine how operands are grouped with operators. This is done via the precedence and associativity rules, which we'll discuss momentarily.
  • At compile time or runtime, the operands are evaluated and operations executed to produce a result.

Operator precedence

To assist with parsing a compound expression, all operators are assigned a level of precedence. Operators with a higher precedence level are grouped with operands first.

You can see in the table below that multiplication and division (precedence level 5) have a higher precedence level than addition and subtraction (precedence level 6). Thus, multiplication and division will be grouped with operands before addition and subtraction. In other words, 6 + 3 * 4 will be grouped as 6 + (3 * 4).

Operator associativity

Consider a compound expression like 12 - 5 - 2. Should this be grouped as (12 - 5) - 2 which evaluates to 5, or 12 - (5 - 2), which evaluates to 9? Since both subtraction operators have the same precedence level, the compiler cannot use precedence alone to determine how this should be grouped.

If two operators with the same precedence level are adjacent to each other in an expression, the operator's associativity tells the compiler whether to evaluate the operators (not the operands!) from left to right or from right to left. Subtraction has precedence level 6, and the operators in precedence level 6 have an associativity of left to right. So this expression is grouped from left to right: (12 - 5) - 2.

Table of operator precedence and associativity

The below table is primarily meant to be a reference chart that you can refer back to in the future to resolve any precedence or associativity questions you have.

Notes:

  • Precedence level 1 is the highest precedence level, and level 17 is the lowest. Operators with a higher precedence level have their operands grouped first.
  • L->R means left to right associativity.
  • R->L means right to left associativity.
Prec/Ass Operator Description Pattern
1 L->R :: Global scope (unary) ::name
:: Namespace scope (binary) class_name::member_name
2 L->R () Parentheses (expression)
() Function call function_name(arguments)
type() Functional cast type(expression)
type{} List init temporary object (C++11) type{expression}
[] Array subscript pointer[expression]
. Member access from object object.member_name
-> Member access from object ptr object_pointer->member_name
++ Post-increment lvalue++
-- Post-decrement lvalue--
typeid Run-time type information typeid(type) or typeid(expression)
const_cast Cast away const const_cast(expression)
dynamic_cast Run-time type-checked cast dynamic_cast(expression)
reinterpret_cast Cast one type to another reinterpret_cast(expression)
static_cast Compile-time type-checked cast static_cast(expression)
sizeof... Get parameter pack size sizeof...(expression)
noexcept Compile-time exception check noexcept(expression)
alignof Get type alignment alignof(type)
3 R->L + Unary plus +expression
- Unary minus -expression
++ Pre-increment ++lvalue
-- Pre-decrement --lvalue
! Logical NOT !expression
not Logical NOT not expression
~ Bitwise NOT ~expression
(type) C-style cast (new_type)expression
sizeof Size in bytes sizeof(type) or sizeof(expression)
co_await Await asynchronous call co_await expression (C++20)
& Address of &lvalue
* Dereference *expression
new Dynamic memory allocation new type
new[] Dynamic array allocation new type[expression]
delete Dynamic memory deletion delete pointer
delete[] Dynamic array deletion delete[] pointer
4 L->R ->* Member pointer selector object_pointer->*pointer_to_member
.* Member object selector object.*pointer_to_member
5 L->R * Multiplication expression * expression
/ Division expression / expression
% Remainder expression % expression
6 L->R + Addition expression + expression
- Subtraction expression - expression
7 L->R << Bitwise shift left / Insertion expression << expression
>> Bitwise shift right / Extraction expression >> expression
8 L->R <=> Three-way comparison (C++20) expression <=> expression
9 L->R < Comparison less than expression < expression
<= Comparison less than or equals expression <= expression
> Comparison greater than expression > expression
>= Comparison greater than or equals expression >= expression
10 L->R == Equality expression == expression
!= Inequality expression != expression
11 L->R & Bitwise AND expression & expression
12 L->R ^ Bitwise XOR expression ^ expression
13 L->R | Bitwise OR expression | expression
14 L->R && Logical AND expression && expression
and Logical AND expression and expression
15 L->R || Logical OR expression || expression
or Logical OR expression or expression
16 R->L throw Throw expression throw expression
co_yield Yield expression (C++20) co_yield expression
?: Conditional expression ? expression : expression
= Assignment lvalue = expression
*= Multiplication assignment lvalue *= expression
/= Division assignment lvalue /= expression
%= Remainder assignment lvalue %= expression
+= Addition assignment lvalue += expression
-= Subtraction assignment lvalue -= expression
<<= Bitwise shift left assignment lvalue <<= expression
>>= Bitwise shift right assignment lvalue >>= expression
&= Bitwise AND assignment lvalue &= expression
|= Bitwise OR assignment lvalue |= expression
^= Bitwise XOR assignment lvalue ^= expression
17 L->R , Comma operator expression, expression

You should already recognize a few of these operators, such as +, -, *, /, (), and sizeof. However, unless you have experience with another programming language, the majority of the operators in this table will probably be incomprehensible to you right now. That's expected at this point. We'll cover many of them in this chapter, and the rest will be introduced as there is a need for them.

Q: Where is the exponent operator?

C++ doesn't include an operator to do exponentiation (operator^ has a different function in C++). We discuss exponentiation more in the Remainder and Exponentiation lesson.

Note that operator<< handles both bitwise left shift and insertion, and operator>> handles both bitwise right shift and extraction. The compiler can determine which operation to perform based on the types of the operands.

Parenthesization

Due to the precedence rules, 6 + 3 * 4 will be grouped as 6 + (3 * 4). But what if we actually meant (6 + 3) * 4? Just like in normal mathematics, in C++ we can explicitly use parentheses to set the grouping of operands as we desire. This works because parentheses have one of the highest precedence levels, so parentheses generally evaluate before whatever is inside them.

Use parenthesis to make compound expressions easier to understand

Now consider an expression like playerScore && enemyScore || bonusActive. Does this evaluate as (playerScore && enemyScore) || bonusActive or playerScore && (enemyScore || bonusActive)? You could look up in the table and see that && takes precedence over ||. But there are so many operators and precedence levels that it's hard to remember them all. And you don't want to have to look up operators all the time to understand how a compound expression evaluates.

In order to reduce mistakes and make your code easier to understand without referencing a precedence table, it's a good idea to parenthesize any non-trivial compound expression, so it's clear what your intent is.

Best Practice
Use parentheses to make it clear how a non-trivial compound expression should evaluate (even if they are technically unnecessary).

A good rule of thumb is: Parenthesize everything, except addition, subtraction, multiplication, and division.

There is one additional exception to the above best practice: Expressions that have a single assignment operator (and no comma operator) do not need to have the right operand of the assignment wrapped in parenthesis.

For example:

totalScore = (playerScore + bonusScore + comboMultiplier);   // instead of this
totalScore = playerScore + bonusScore + comboMultiplier;     // it's okay to do this

totalScore = ((playerWon || enemyDefeated) && levelComplete); // instead of this
totalScore = (playerWon || enemyDefeated) && levelComplete;   // it's okay to do this

totalScore = (playerScore *= bonusMultiplier); // expressions with multiple assignments still benefit from parenthesis

The assignment operators have the second lowest precedence (only the comma operator is lower, and it's rarely used). Therefore, so long as there is only one assignment (and no commas), we know the right operand will fully evaluate before the assignment.

Best Practice
Expressions with a single assignment operator do not need to have the right operand of the assignment wrapped in parenthesis.

Value computation of operations

The C++ standard uses the term value computation to mean the execution of operators in an expression to produce a value. The precedence and association rules determine the order in which value computation happens.

For example, given the expression 6 + 3 * 4, due to the precedence rules this groups as 6 + (3 * 4). The value computation for (3 * 4) must happen first, so that the value computation for 6 + 12 can be completed.

Evaluation of operands

The C++ standard (mostly) uses the term evaluation to refer to the evaluation of operands (not the evaluation of operators or expressions!). For example, given expression playerScore + bonusScore, playerScore will be evaluated to produce some value, and bonusScore will be evaluated to produce some value. These values can then be used as operands to operator+ for value computation.

Nomenclature: Informally, we typically use the term "evaluates" to mean the evaluation of an entire expression (value computation), not just the operands of an expression.

The order of evaluation of operands (including function arguments) is mostly unspecified

In most cases, the order of evaluation for operands and function arguments is unspecified, meaning they may be evaluated in any order.

Consider the following expression:

playerScore * bonusMultiplier + enemyScore * penaltyMultiplier

We know from the precedence and associativity rules above that this expression will be grouped as if we had typed:

(playerScore * bonusMultiplier) + (enemyScore * penaltyMultiplier)

If playerScore is 100, bonusMultiplier is 2, enemyScore is 50, and penaltyMultiplier is 3, this expression will always compute the value 350.

However, the precedence and associativity rules only tell us how operators and operands are grouped and the order in which value computation will occur. They do not tell us the order in which the operands or subexpressions are evaluated. The compiler is free to evaluate operands playerScore, bonusMultiplier, enemyScore, or penaltyMultiplier in any order. The compiler is also free to calculate playerScore * bonusMultiplier or enemyScore * penaltyMultiplier first.

For most expressions, this is irrelevant. In our sample expression above, it doesn't matter in which order variables playerScore, bonusMultiplier, enemyScore, or penaltyMultiplier are evaluated for their values: the value calculated will always be 350. There is no ambiguity here.

But it is possible to write expressions where the order of evaluation does matter. Consider this program, which contains a mistake often made by new C++ programmers:

#include <iostream>

int getScore()
{
    std::cout << "Enter a score: ";

    int score {};
    std::cin >> score;
    return score;
}

void printTotal(int score1, int score2, int score3)
{
    std::cout << score1 + (score2 * score3);
}

int main()
{
    printTotal(getScore(), getScore(), getScore()); // this line is ambiguous

    return 0;
}

If you run this program and enter the inputs 10, 20, and 30, you might assume that this program would calculate 10 + (20 * 30) and print 610. But that is making the assumption that the arguments to printTotal() will evaluate in left-to-right order (so parameter score1 gets value 10, score2 gets value 20, and score3 gets value 30). If instead, the arguments evaluate in right-to-left order (so parameter score3 gets value 10, score2 gets value 20, and score1 gets value 30), then the program will print 630 instead.

Tip: The Clang compiler evaluates arguments in left-to-right order. The GCC compiler evaluates arguments in right-to-left order.

If you'd like to see this behavior for yourself, you can do so on Wandbox. Paste in the above program, enter 10 20 30 in the Stdin tab, select GCC or Clang, and then compile the program. The output will appear at the bottom of the page (you may have to scroll down to see it). You will note that the output for GCC and Clang differs!

The above program can be made unambiguous by making each function call to getScore() a separate statement:

#include <iostream>

int getScore()
{
    std::cout << "Enter a score: ";

    int score {};
    std::cin >> score;
    return score;
}

void printTotal(int score1, int score2, int score3)
{
    std::cout << score1 + (score2 * score3);
}

int main()
{
    int firstScore { getScore() };  // will execute first
    int secondScore { getScore() }; // will execute second
    int thirdScore { getScore() };  // will execute third

    printTotal(firstScore, secondScore, thirdScore); // this line is now unambiguous

    return 0;
}

In this version, firstScore will always have value 10, secondScore will have value 20, and thirdScore will have value 30. When the arguments to printTotal() are evaluated, it doesn't matter which order the argument evaluation happens in -- parameter score1 will always get value 10, score2 will get value 20, and score3 will get value 30. This version will deterministically print 610.

Key Concept
Operands, function arguments, and subexpressions may be evaluated in any order.

It is a common mistake to believe that operator precedence and associativity affects order of evaluation. Precedence and associativity is used only to determine how operands are grouped with operators, and the order of value computation.

Warning
Ensure that the expressions (or function calls) you write are not dependent on operand (or argument) evaluation order.

Related content: Operators with side effects can also cause unexpected evaluation results. We cover this in the Increment/decrement operators, and side effects lesson.

Summary

  • Operator precedence: All operators are assigned a precedence level; operators with higher precedence are grouped with operands first
  • Operator associativity: When operators have the same precedence level, associativity determines whether they evaluate left-to-right or right-to-left
  • Parentheses: Have one of the highest precedence levels; use them to explicitly control grouping in compound expressions
  • Best practices:
    • Parenthesize non-trivial compound expressions to make intent clear
    • Rule of thumb: Parenthesize everything except addition, subtraction, multiplication, and division
    • Expressions with a single assignment don't need parentheses around the right operand
  • Value computation: The execution of operators to produce a value; order is determined by precedence and associativity
  • Operand evaluation: The evaluation of operands happens separately from value computation
  • Unspecified evaluation order: In most cases, the order in which operands and function arguments are evaluated is unspecified and may vary between compilers
  • Compiler differences: Clang evaluates function arguments left-to-right, GCC evaluates right-to-left
  • Common mistake: Assuming precedence/associativity determines operand evaluation order (it doesn't - it only determines grouping)
  • Safe coding: Avoid writing expressions or function calls that depend on the order of operand or argument evaluation

Understanding operator precedence and associativity helps you write clear, unambiguous expressions. When in doubt, use parentheses to make your intent explicit rather than relying on implicit precedence rules.