Operator Precedence

Concepts and Ranking of C++ Operators

C++ has more operators than C — it is actually a superset of the C operators. This do­cu­ment is simp­ly a convenient quick reference, with a few notes re­gard­ing some im­por­tant op­e­ra­tors and the types of their operands. Operator overloading is also sum­ma­ris­ed, along with the con­cepts and ter­mi­no­lo­gy related to operators and C/C++ expressions.

PREREQUISITES — You should already…
  • have some C programming experience, or introductory C++ experience;
  • understand lvalues, rvalues, expressions, and function signatures.

Introduction

It is important to understand the behaviour of operators as a two step process:

  1. Perform a task (the name of the operator is indicative of the task).
  2. Result in a value / return a value.

When that is understood, it is then simple to explain the difference between, for example, the pre-increment and post-increment operators: They perform the exact same task or job, but pro­duce dif­fer­ent results. So, if the return value is not used in an expression, it is irrelevant whether the pre-, or post-fix operator is used.

Operand Types & Returns

All operators have rules regarding the type of their operands. Some operators require operands of the same type, like the arithmetic operators. The compiler will promote a lower ranked type to the other operand's type, before the operator is invoked. The result will be the type of the op­er­and with the high­est ranked type.

Most operators return rvalues, i.e., values that cannot be modified. Only a few, like the in­di­rec­tion op­e­ra­tor, can potentially result in lvalues (expressions representing modifiable memory). Conceptually, think of operators as functions returning values. Some operators, like mem­ber se­lec­tion, and in­di­rect mem­ber selection, will return lvalues, if the left-hand operand is an lvalue, or points to an lvalue. Subscripting is just indirection in disguise, so all rules that relate to in­di­rec­tion, also apply to subscripting, such as: produce lvalues, if the left-hand operand is a not a void* type, or a const T* type.

Operand Association or Grouping

When the compiler must choose between two operators with the same level of precedence, it considers how they associate with respect to their operands: from left-to-right, or right-to-left, and chooses the first operator to create code for, on that basis. Most operators associate from left-to-right. Significantly, the assignment operators, and the unary prefix operators, associate from right-to-left. This is illustrated in the following code extract:

The expression in the last statement is evaluated as follows:

On the other hand, postfix increment/decrement has higher precedence than indirection, so as­so­ci­a­tion does not apply in the following expression:

However, prefix increment/decrement and indirection have the same precedence, therefore op­er­and association does play a role:

Understanding operand association direction, is as important as understanding and learning the operator precedence table.

Sequence Guarantees

Many operators, in particular the arithmetic operators, do not provide any standardised order of evaluation of the operands. In the following code extract, which of the two operand expressions is evaluated first, f() or g(), is implementation defined. That is because the addition operator does not provide a guarantee of the sequence. Neither of the functions should depend on side-effects produced by the other.

A handful of operators provide a sequence guarantee:
&& (logical AND),
|| (logical OR),
, (comma), and the
?: (conditional operator).

Their left-hand operands are guaranteed to be evaluated first.

In the above statement, the f() expression is guaranteed to be evaluated first, because of the sequence guarantee provided by the logical AND operator (&&).

The binary logical operators, and the conditional operator, do not automatically evaluate their right operands. For the two logical operators (&& and ||), the right-hand operand is only eval­u­at­ed if the re­sult can­not be determined from the first operand. This is called short-circuiting.

The conditional operator takes three operands. The result of the op­e­ra­tor will be either the re­sult of the 2nd operand, or the result of the 3rd operand, based on the logical value of the 1st operand. It therefore only ever evaluates two operands: the first, and based in its result, one of the other two.

The comma operator is easily abused, leading to unreadable and buggy code, and should only be used in well-established, recognisable patterns.

Operator Precedence Table

Operators are arranged in decreasing priority. Operators on the same level, have the same as­so­cia­tion.

No. Operator Name Ass.
1 N :: I Scope resolution. N: namespace/class, I: identifier. L→R
2 L ++L -- Post-inc/decrement. L: lvalue L→R
T ( E )T { E } Functional cast. T: type, E: expression.
F ( A ) Function call. F: function ptr., A: arguments.
P [ I ] Subscript. P: pointer expr., I: integer.
S . M Member select. S: structured expr., M: member identifier.
P -> M Indirect member select. E: pointer expr., M: member identifier.
3 ++ LV-- LV Pre-inc/decrement. LV: lvalue R→L
- N+ N Negation.Identity N: numerical type expr.
! B~ I Logical NOTComplement (bitwise NOT) B: bool, I: integer.
( T ) E C-style cast. T: type, E: expression.
* P Indirection. P: pointer expr.
& M Address-of. M: Expr. representing memory.
sizeof( T ) Sizeof Type. T: type.
sizeof E Sizeof Expression. E: expr.
new T Dynamic Memory Allocator. T: type.
new T [ N ] Dynamic C-Style Array Allocator. T: type, N: integer.
delete P Release Dynamic Memory. P: pointer expr.
delete[] P Release Dynamic C-Style Array Memory. P: pointer expr.
4 S .* P Pointer-to-Member Select.
S: struct | class obj., P: ptr-to-member expr.
L→R
P ->* Q Indirect Pointer-to-Member Select.
P: struct | class ptr., Q: ptr-to-member expr.
5 A1 * A2 Multiplication. A: arithmetic expr. L→R
A1 / A2 Division. A: arithmetic expr.
I1 % I2 Modulus / Remainder. I: integer expr.
6 A1 * A2 Addition. A: arithmetic expr. L→R
A1 / A2 Subtraction. A: arithmetic expr.
7 I1 << I2 Bitwise Left-Shift. I: integer expr. L→R
I1 >> I2 Bitwise Right-Shift. I: integer expr.
8 E1 < E2 Less Than / Smaller Than. E: expr. L→R
E1 > E2 Greater Than / Bigger Than. E: expr.
E1 <= E2 Less Than or Equal. E: expr.
E1 >= E2 Greater Than or Equal. E: expr.
9 E1 == E2 Equality. E: expr. L→R
E1 != E2 Inequality. E: expr.
10 I1 & I2 Bitwise AND. I: integer expr. L→R
11 I1 ^ I2 Bitwise XOR. I: integer expr. L→R
12 I1 | I2 Bitwise OR. I: integer expr. L→R
13 B1 && B2 Logical AND. B: bool expr. L→R
14 B1 || B2 Logical OR. B: bool expr. L→R
15 Eb ? Et : Ef Conditional. El: bool expr., Et: “true” expr., Ef: “false” expr. R→L
throw E Exception Throw. E: expr.
L = E Assignment. L: lvalue expr., E: expr.
L ▢= E Compound Assignment. L: lvalue expr., E: expr.,
: + - * / % << >> & ^ |
16 E1 , E2 Comma. E: expr. L→R

Operator Overloading

Most operators can be overloaded. Some operators must be overloaded as member functions in­side a class. Operator expressions are treated as function calls, albeit functions with names start­ing with ‘operator’. Once the compiler has established a function signature based on (a) the function name (op­e­ra­tor name), (b) the type of arguments (operands), and (c) the number of ar­gu­ments (operands), it will look for such a “function“, in exactly the same way it looks for normal functions.

So the term “operator overloading” is really just the same mechanism as normal function over­load­ing — entirely based on signatures. It is made possible because of an independent mech­a­nism, name­ly the treatment of operators as function calls, albeit functions with “strange” names. These op­e­ra­tor functions can be called by name, just like other functions, but generally, we pre­fer the op­e­ra­tor syntax. The signatures for operators on intrinsic types are not allowed, and also cannot be called, using function call notation.

Basic Syntax

Operators that are overloadable, are functions with one of these patterns, where indicates one of the operator characters, P indicates parameters, and T indicates a valid return type. Whitespace be­tween and the operator keyword, or the left parenthesis, is allowed, and not significant.

The …operator new… actually has many variations; only the basic versions are shown. The same ap­plies to …operator delete….

Overloadable Operators

Possible values for above:
    + (unary and binary), - (unary and binary),
    * (multiplication and indirection),
    /, %, ^,
    &, |, ~, !,
    =, <, >, <=, >=, ==, !=,
    +=, -=, *=, /=, %=, ^=,
    &=, |=, <<, >>,
    &&, ||,
    ++ (prefix and postfix), -- (prefix and postfix),
    , (comma),
    ->*, ->,
    () (function call),
    [] (subscript).

The operators that can only be overloaded as member functions:
    = — assignment,
    [] — subscript,
    () — function call.

Further Rules

New operators cannot be created.

The precedence of operators cannot be changed.

The number of operands cannot be changed.

Normally commutative operators are not commutative when overloaded.

Sequence guarantees are lost when overloading &&, || and , (comma) operators.

The scope resolution (::), member selection (.), ptr-to-member select (.*) and conditional (?:) operators cannot be overloaded at all.

The indirection member selection operator (->) is restricted to either returning an expression for which -> is valid: a raw pointer, or an object whose type overloaded ->.


2017-11-18: Update to new admonitions. [brx]
2017-09-23: Editing and formatting. [jjc]
2016-07-10: Created. [brx]