1.3 Operators
C++ is rich in built-in operators. There are different kinds of operators:
Computational:
– Arithmetic: ++, +, *, %, ...
– Boolean:
* Comparison: <=, !=, ...
* Logic: && and ||
– Bitwise: ~, ≪ and ≫, &, ^, and |
Assignment: =, +=, ...
Program flow: function call, ?:, and ,
Memory handling: new and delete
Access: ., ->, [ ], *, ...
Type handling: dynamic_cast, typeid, sizeof, alignof, ...
Error handling: throw
This section will give you an overview of the operators. Some operators are better described elsewhere in the context of the appropriate language feature; e.g., scope resolution is best explained together with namespaces. Most operators can be overloaded for user types; i.e., we can decide which calculations are performed when arguments of our types appear in expressions.
At the end of this section (Table 1–8), you will find a concise table of operator precedence. It might be a good idea to print or copy this page and pin it next to your monitor; many people do so and almost nobody knows the entire priority list by heart. Neither should you hesitate to put parentheses around sub-expressions if you are uncertain about the priorities or if you believe it will be more understandable for other programmers working with your sources. If you ask your compiler to be pedantic, it often takes this job too seriously and prompts you to add surplus parentheses assuming you are overwhelmed by the precedence rules. In Section C.2, we will give you a complete list of all operators with brief descriptions and references.
1.3.1 Arithmetic Operators
Table 1–2 lists the arithmetic operators available in C++. We have sorted them by their priorities, but let us look at them one by one.
Table 1–2: Arithmetic Operators
Operation |
Expression |
---|---|
Post-increment |
x++ |
Post-decrement |
x-- |
Pre-increment |
++x |
Pre-decrement |
--x |
Unary plus |
+x |
Unary minus |
-x |
Multiplication |
x * y |
Division |
x / y |
Modulo |
x % y |
Addition |
x + y |
Subtraction |
x – y |
The first kinds of operations are increment and decrement. These operations can be used to increase or decrease a number by 1. As they change the value of the number, they only make sense for variables and not for temporary results, for instance:
int i= 3; i++; // i is now 4 const int j= 5; j++; // Error: j is constant (3 + 5)++; // Error: 3 + 5 is only a temporary
In short, the increment and decrement operations need something that is modifiable and addressable. The technical term for an addressable data item is lvalue (more formally expressed in Definition C–1 in Appendix C). In our code snippet above, this is true for i only. In contrast to it, j is constant and 3 + 5 is not addressable.
Both notations—prefix and postfix—have the effect on a variable that they add or subtract 1 from it. The value of an increment and decrement expression is different for prefix and postfix operators: the prefix operators return the modified value and postfix the old one, e.g.:
int i= 3, j= 3; int k= ++i + 4; // i is 4, k is 8 int l= j++ + 4; // j is 4, l is 7
At the end, both i and j are 4. However in the calculation of l, the old value of j was used while the first addition used the already incremented value of i.
In general, it is better to refrain from using increment and decrement in mathematical expressions and to replace it with j+1 and the like or to perform the in/decrement separately. It is easier for human readers to understand and for the compiler to optimize when mathematical expressions have no Side Effects. We will see quite soon why (§1.3.12).
The unary minus negates the value of a number:
int i= 3; int j= -i; // j is -3
The unary plus has no arithmetic effect on standard types. For user types, we can define the behavior of both unary plus and minus. As shown in Table 1–2, these unary operators have the same priority as pre-increment and pre-decrement.
The operations * and / are naturally multiplication and division, and both are defined on all numeric types. When both arguments in a division are integers, the fractional part of the result is truncated (rounding toward zero). The operator % yields the remainder of the integer division. Thus, both arguments should have an integral type.
Last but not least, the operators + and - between two variables or expressions symbolize addition and subtraction.
The semantic details of the operations—how results are rounded or how overflow is handled—are not specified in the language. For performance reasons, C++ leaves this typically to the underlying hardware.
In general, unary operators have higher priority than binary. On the rare occasions that both postfix and prefix unary notations have been applied, postfix notations are prioritized over prefix notations.
Among the binary operators, we have the same behavior that we know from math: multiplication and division precede addition and subtraction and the operations are left associative, i.e.:
x - y + z
is always interpreted as
(x - y) + z
Something really important to remember: the order of evaluation of the arguments is not defined. For instance:
int i= 3, j= 7, k; k= f(++i) + g(++i) + j;
In this example, associativity guarantees that the first addition is performed before the second. But whether the expression f(++i) or g(++i) is computed first depends on the compiler implementation. Thus, k might be either f(4) + g(5) + 7 or f(5) + g(4) + 7 (or even f(5) + g(5) + 7 when both increments are executed before the function call). Furthermore, we cannot assume that the result is the same on a different platform. In general, it is dangerous to modify values within expressions. It works under some conditions, but we always have to test it and pay enormous attention to it. Altogether, our time is better spent by typing some extra letters and doing the modifications separately. More about this topic in Section 1.3.12.
⇒ c++03/num_1.cpp
With these operators, we can write our first (complete) numeric program:
#include <iostream> int main () { const float r1= 3.5, r2 = 7.3, pi = 3.14159; float area1 = pi * r1*r1; std::cout ≪ "A circle of radius " ≪ r1 ≪ " has area " ≪ area1 ≪ "." ≪ std::endl; std::cout ≪ "The average of " ≪ r1 ≪ " and " ≪ r2 ≪ " is " ≪ (r1 + r2) / 2 ≪ "." ≪ std::endl; }
When the arguments of a binary operation have different types, one or both arguments are automatically converted (coerced) to a common type according to the rules in §C.3.
The conversion may lead to a loss of precision. Floating-point numbers are preferred over integer numbers, and evidently the conversion of a 64-bit long to a 32-bit float yields an accuracy loss; even a 32-bit int cannot always be represented correctly as a 32-bit float since some bits are needed for the exponent. There are also cases where the target variable could hold the correct result but the accuracy was already lost in the intermediate calculations. To illustrate this conversion behavior, let us look at the following example:
long l= 1234567890123; long l2= l + 1.0f - 1.0; // imprecise long l3= l + (1.0f - 1.0); // correct
This leads on the author’s platform to
l2 = 1234567954431 l3 = 1234567890123
In the case of l2 we lose accuracy due to the intermediate conversions, whereas l3 was computed correctly. This is admittedly an artificial example, but you should be aware of the risk of imprecise intermediate results. Especially with large calculations the numerical algorithms must be carefully chosen to prevent the errors from building up. The issue of inaccuracy will fortunately not bother us in the next section.
1.3.2 Boolean Operators
Boolean operators are logical and relational operators. Both return bool values as the name suggests. These operators and their meaning are listed in Table 1–3, grouped by precedence.
Binary relational and logical operators are preceded by all arithmetic operators. This means that an expression like 4 >= 1 + 7 is evaluated as if it were written 4 >= (1 + 7). Conversely, the unary operator ! for logic negation is prioritized over all binary operators.
Table 1–3: Boolean Operators
Operation |
Expression |
---|---|
Not |
!b |
Three-way comparison (C++20) |
x <=> y |
Greater than |
x > y |
Greater than or equal to |
x >= y |
Less than |
x < y |
Less than or equal to |
x <= y |
Equal to |
x == y |
Not equal to |
x != y |
Logical AND |
b && c |
Logical OR |
b || c |
The boolean operators also have keywords, like not, and, or, and xor. There are even keywords for assignments, like or_eq for |=. We usually don’t use them for their paleolithic look but there is one exception: not can make expressions far more readable. When negating something that starts with “i” or “l”, the exclamation point is easily overseen. A space already helps, but the keyword makes the negation even more visible:
big= !little; // You knew before there's an ! big= not little; // Much easier to spot, though
Although these keywords have been available from the beginning of standard C++, Visual Studio still doesn’t support them unless you compile with /permissive- or /Za.
In old (or old-fashioned) code, you might see logical operations performed on int values. Please refrain from this: it is less readable and subject to unexpected behavior.
Please note that comparisons cannot be chained like this:
bool in_bound= min <= x <= y <= max; // Syntax error
Instead we need the more verbose logical reduction:
bool in_bound= min <= x && x <= y && y <= max;
In the following section, we will see similar operators.
1.3.3 Bitwise Operators
These operators allow us to test or manipulate single bits of integral types. They are important for system programming but less so for modern application development. Table 1–4 lists these operators by precedence.
The operation x ≪ y shifts the bits of x to the left by y positions. Conversely, x ≫ y moves x’s bits y times to the right.6 In most cases, 0s are moved (except for negative signed values) to the right where they are implementation defined.
Table 1–4: Bitwise Operators
Operation |
Expression |
---|---|
One’s complement |
∼x |
Left shift |
x ≪ y |
Right shift |
x ≫ y |
Bitwise AND |
x & y |
Bitwise exclusive OR |
x ^ y |
Bitwise inclusive OR |
x | y |
The bitwise AND can be used to test a specific bit of a value. Bitwise inclusive OR can set a bit and exclusive OR flips it. Although these operations are less important in scientific applications, we will use them in Section 3.5.1 for algorithmic entertainment.
1.3.4 Assignment
The value of an object (modifiable lvalue) can be set by an assignment:
object= expr;
When the types do not match, expr is converted to the type of object if possible. The assignment is right-associative so that a value can be successively assigned to multiple objects in one expression:
o3= o2= o1= expr;
Speaking of assignments, the author will now explain why he left-justifies the symbol. Most binary operators are symmetric in the sense that both arguments are values. In contrast, an assignment must have a modifiable variable on the left-hand side whereby the right-hand side can be an arbitrary expression (with an appropriate value). While other languages use asymmetric symbols (e.g., := in Pascal), the author uses an asymmetric spacing in C++.
The compound assignment operators apply an arithmetic or bitwise operation to the object on the left side with the argument on the right side; for instance, the following two operations are equivalent:
a+= b; // corresponds to a= a + b;
All assignment operators have a lower precedence than every arithmetic or bitwise operation so the right-hand side expression is always evaluated before the compound assignment:
a*= b + c; // corresponds to a= a * (b + c);
The assignment operators are listed in Table 1–5. They are all right-associative and of the same priority.
1.3.5 Program Flow
There are three operators to control the program flow. First, a function call in C++ is handled like an operator. For a detailed description of functions and their calls, see Section 1.5.
Table 1–5: Assignment Operators
Operation |
Expression |
---|---|
Simple assignment |
x= y |
Multiply and assign |
x*= y |
Divide and assign |
x/= y |
Modulo and assign |
x%= y |
Add and assign |
x+= y |
Subtract and assign |
x-= y |
Shift left and assign |
x≪= y |
Shift right and assign |
x≫= y |
AND and assign |
x&= y |
Inclusive OR and assign |
x|= y |
Exclusive OR and assign |
x^= y |
The conditional operator c ? x : y evaluates the condition c, and when it is true the expression has the value of x, otherwise y. It can be used as an alternative to branches with if, especially in places where only an expression is allowed and not a statement; see Section 1.4.3.1.
A very special operator in C++ is the Comma Operator that provides a sequential evaluation. The meaning is simply evaluating first the subexpression to the left of the comma and then that to the right of it. The value of the whole expression is that of the right subexpression:
3 + 4, 7 * 9.3
The result of the expression is 65.1 and the computation of the first subexpression is entirely irrelevant in this case. The subexpressions can contain the comma operator as well so that arbitrarily long sequences can be defined. With the help of the comma operator, one can evaluate multiple expressions in program locations where only one expression is allowed. A typical example is the increment of multiple indices in a for-loop (§1.4.4.2):
++i, ++j
When used as a function argument, the comma expression needs surrounding parentheses; otherwise the comma is interpreted as separation of function arguments.
1.3.6 Memory Handling
The operators new and delete allocate and deallocate memory, respectively. We postpone their description to Section 1.8.2 since discussing these operators before talking about pointers makes no sense.
1.3.7 Access Operators
C++ provides several operators for accessing substructures, for referring—i.e., taking the address of a variable—and dereferencing—i.e., accessing the memory referred to by an address. They are listed in Table 1–6. We will demonstrate in Section 2.2.3 how to use them, after we introduce pointers and classes.
Table 1–6: Access Operators
Operation |
Expression |
Reference |
---|---|---|
Member selection |
x.m |
§2.2.3 |
Dereferred member selection |
p->m |
§2.2.3 |
Subscripting |
x[i] |
§1.8.1 |
Dereference |
*x |
§1.8.2 |
Address-of |
&x |
§1.8.2 |
Member dereference |
x.*q |
§2.2.3 |
Dereferred member dereference |
p->*q |
§2.2.3 |
1.3.8 Type Handling
The operators for dealing with types will be presented in Chapter 5 when we will write compile-time programs that work on types. Right now we only list them in Table 1–7.
Table 1–7: Type-Handling Operators
Operation |
Expression |
---|---|
Run-time type identification |
typeid(x) |
Identification of a type |
typeid(t) |
Size of object |
sizeof(x) or sizeof x |
Size of type |
sizeof(t) |
Number of arguments |
sizeof...(p) |
Number of type arguments |
sizeof...(P) |
Alignment of object (C++11) |
alignof(x) |
Alignment of type (C++11) |
alignof(t) |
Note that the sizeof operator when used on an expression is the only one that is applicable without parentheses. alignof was introduced in C++11; all others have existed since C++98 (at least).
1.3.9 Error Handling
The throw operator is used to indicate an exception in the execution (e.g., insufficient memory); see Section 1.6.2.
1.3.10 Overloading
A very powerful aspect of C++ is that the programmer can define operators for new types. This will be explained in Section 2.7. Operators of built-in types cannot be changed. However, we can define how built-in types interact with new types; i.e., we can overload mixed operations like double times matrix. Most operators can be overloaded. Exceptions are:
:: |
Scope resolution; |
. |
Member selection; |
.* |
Member selection through pointer; |
?: |
Conditional; |
sizeof |
Size of a type or object; |
sizeof... |
Number of arguments; |
alignof |
Memory alignment of a type or object; and |
typeid |
Type identifier. |
The operator overloading in C++ gives us a lot of freedom and we have to use this freedom wisely. We come back to this topic in the next chapter when we actually overload operators (in Section 2.7).
1.3.11 Operator Precedence
Table 1–8 gives a concise overview of the operator priorities. For compactness, we combined notations for types and expressions (e.g., typeid) and fused the different notations for new and delete. The symbol @= represents all computational assignments like +=, -=, and so on. A more detailed summary of operators with semantics is given in Appendix C, Table C–1.
Table 1–8: Operator Precedence
Operator Precedence |
|||
---|---|---|---|
class::member |
nspace::member |
::name |
::qualified-name |
object.member |
pointer->member |
expr[expr] |
expr(expr list) |
type(expr list) |
lvalue++ |
lvalue-- |
typeid(type/expr |
*_cast<type>(expr) |
|||
sizeof expr |
sizeof(type) |
sizeof...(pack ) |
alignof(type/expr) |
++lvalue |
--lvalue |
∼expr |
!expr |
-expr |
+expr |
&lvalue |
*expr |
new . . . type. . . |
delete []opt pointer |
(type) expr |
co_await expr |
object.*member ptr |
pointer->*member ptr |
||
expr * expr |
expr / expr |
expr % expr |
|
expr + expr |
expr - expr |
||
expr ≪ expr |
expr ≫ expr |
||
expr <=> expr |
|||
expr < expr |
expr <= expr |
expr > expr |
expr >= expr |
expr == expr |
expr != expr |
||
expr & expr |
|||
expr ^ expr |
|||
expr | expr |
|||
expr && expr |
|||
expr || expr |
|||
expr ? expr: expr |
|||
lvalue = expr |
lvalue @= expr |
throw expr |
co_yield expr |
expr, expr |
1.3.12 Avoid Side Effects!
“Insanity: doing the same thing over and over again and expecting different results.”
—Unknown7
In applications with side effects it is not insane to expect a different result for the same input. On the contrary, it is very difficult to predict the behavior of a program whose components interfere massively. Moreover, it is probably better to have a deterministic program with the wrong result than one that occasionally yields the right result since the latter is usually much harder to fix.
An example where the side effects are incorporated correctly is the string copy function strcpy from the C standard library. The function takes pointers to the first char of the source and the target and copies the subsequent letters until it finds a zero. This can be implemented with one single loop that even has an empty body and performs the copy and the increments as side effects of the continuation test:
while (*tgt++= *src++);
Looks scary? Well, it somehow is. Nonetheless, this is absolutely legal C++ code, although some compilers might grumble in pedantic mode. It is a good mental exercise to spend some time thinking about operator priorities, types of subexpressions, and evaluation order.
Let us think about something simpler: we assign the value i to the i-th entry of an array and increment the value i for the next iteration:
v[i]= i++; // Undefined behavior before C++17
Looks like no problem. Before C++17 it was: the behavior of this expression was undefined. Why? The post-increment of i guarantees that we assign the old value of i and increment i afterward. However, this increment can still be performed before the expression v[i] is evaluated so that we possibly assign i to v[i+1]. Well, this was fixed in C++17 by requiring that the entire expression to the right of the assignment must be finished before the left-hand side is evaluated. This doesn’t mean that all undefined behavior disappeared in the meantime. The following—admittedly nasty—expression is still undefined:
i = ++i + i++;
The last examples should give you an impression that side effects are not always evident at first glance. Some quite tricky stuff might work but much simpler things might not. Even worse, something might work for a while until somebody compiles it on a different compiler or the new release of your compiler changes some implementation details.
The first snippet is an example of excellent programming skills and evidence that the operator precedence makes sense—no parentheses were needed. Nonetheless, such programming style is not appropriate for modern C++. The eagerness to shorten code as much as possible dates back to the times of early C when typing was more demanding, with typewriters that were more mechanical than electrical, and card punchers, all without a monitor. With today’s technology, it should not be an issue to type some extra letters.
Another unfavorable aspect of the terse copy implementation is the mingling of different concerns: testing, modification, and traversal. An important concept in software design is Separation of Concerns. It contributes to increasing flexibility and decreasing complexity. In this case, we want to decrease the complexity of the mental processes needed to understand the implementation. Applying the principle to the infamous copy one-liner could yield:
for (; *src; tgt++, src++) *tgt= *src; *tgt= *src; // copy the final 0
Now, we can clearly distinguish the three concerns:
Testing: *src
Modification: *tgt= *src;
Traversal: tgt++, src++
It is also more apparent that the incrementing is performed on the pointers and the testing and assignment on their referred content. The implementation is not as compact as before, but it is much easier to check the correctness. It is also advisable to make the nonzero test more obvious (*src != 0).
There is a class of programming languages that are called Functional Languages. Values in these languages cannot be changed once they are set. C++ is obviously not that way. But we do ourselves a big favor when we program as much as is reasonable in a functional style. For instance, when we write an assignment, the only thing that should change is the variable to the left of the assignment symbol. To this end, we have to replace mutating with a constant expression: for instance, ++i with i+1. A right-hand side expression without side effects helps us understand the program behavior and makes it easier for the compiler to optimize the code. As a rule of thumb: more comprehensible programs have a better potential for optimization. Speaking of which, const declarations not only protect us against accidental modifications, they are also an easy way to enable more optimizations.