Concepts and Ranking of C++ Operators
PREREQUISITES — You should already…
- have some C programming experience, or introductory C++ experience;
- understand lvalues, rvalues, expressions, and function signatures.
It is important to understand the behaviour of operators as a two step process:
- Perform a task (the name of the operator is indicative of the task).
- 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 produce different 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 operand with the highest ranked type.
Most operators return rvalues, i.e., values that cannot be modified. Only a few, like the indirection operator, can potentially result in lvalues (expressions representing modifiable memory). Conceptually, think of operators as functions returning values. Some operators, like member selection, and indirect member 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 indirection, 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 association does not apply in the following expression:
However, prefix increment/decrement and indirection have the same precedence, therefore operand association does play a role:
Understanding operand association direction, is as important as understanding and learning the operator precedence table.
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,
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 (
||), the right-hand operand is only evaluated if the result cannot be determined from the first operand. This is called short-circuiting.
The conditional operator takes three operands. The result of the operator will be either the result 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 association.
||Scope resolution. N: namespace/class, I: identifier.||L→R|
||Post-inc/decrement. L: lvalue||L→R|
||Functional cast. T: type, E: expression.|
||Function call. F: function ptr., A: arguments.|
||Subscript. P: pointer expr., I: integer.|
||Member select. S: structured expr., M: member identifier.|
||Indirect member select. E: pointer expr., M: member identifier.|
||Pre-inc/decrement. LV: lvalue||R→L|
||Negation. │ Identity N: numerical type expr.|
||Logical NOT │ Complement (bitwise NOT) B:
||C-style cast. T: type, E: expression.|
||Indirection. P: pointer expr.|
||Address-of. M: Expr. representing memory.|
||Sizeof Type. T: type.|
||Sizeof Expression. E: expr.|
||Dynamic Memory Allocator. T: type.|
||Dynamic C-Style Array Allocator. T: type, N: integer.|
||Release Dynamic Memory. P: pointer expr.|
||Release Dynamic C-Style Array Memory. P: pointer expr.|
||Indirect Pointer-to-Member Select.
||Multiplication. A: arithmetic expr.||L→R|
||Division. A: arithmetic expr.|
||Modulus / Remainder. I: integer expr.|
||Addition. A: arithmetic expr.||L→R|
||Subtraction. A: arithmetic expr.|
||Bitwise Left-Shift. I: integer expr.||L→R|
||Bitwise Right-Shift. I: integer expr.|
||Less Than / Smaller Than. E: expr.||L→R|
||Greater Than / Bigger Than. E: expr.|
||Less Than or Equal. E: expr.|
||Greater Than or Equal. E: expr.|
||Equality. E: expr.||L→R|
||Inequality. E: expr.|
||Bitwise AND. I: integer expr.||L→R|
||Bitwise XOR. I: integer expr.||L→R|
||Bitwise OR. I: integer expr.||L→R|
||Logical AND. B:
||Logical OR. B:
||Exception Throw. E: expr.|
||Assignment. L: lvalue expr., E: expr.|
||Compound Assignment. L: lvalue expr., E: expr.,
||Comma. E: expr.||L→R|
Most operators can be overloaded. Some operators must be overloaded as member functions inside a class. Operator expressions are treated as function calls, albeit functions with names starting with ‘
operator’. Once the compiler has established a function signature based on (a) the function name (operator name), (b) the type of arguments (operands), and (c) the number of arguments (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 overloading — entirely based on signatures. It is made possible because of an independent mechanism, namely the treatment of operators as function calls, albeit functions with “strange” names. These operator functions can be called by name, just like other functions, but generally, we prefer the operator syntax. The signatures for operators on intrinsic types are not allowed, and also cannot be called, using function call notation.
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 between
▢ and the
operator keyword, or the left parenthesis, is allowed, and not significant.
)— Unary and binary operators. T is return type.
)— Type cast to T operator. Notice that no return type is allowed.
void* operator new (size_t)— Single type dynamic memory allocator.
void* operator new (size_t)— Array type dynamic memory allocator.
)— User-defined literal suffixes operator (C++11).
Possible values for
+ (unary and binary),
- (unary and binary),
* (multiplication and indirection),
++ (prefix and postfix),
-- (prefix and postfix),
() (function call),
The operators that can only be overloaded as member functions:
= — assignment,
 — subscript,
() — function call.
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
, (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]