C++: The Basics - eps1.12_Operators-p2
Hello and welcome. Since we didn’t finish with the topic Operators last time, we’ll follow up directly without much preface.
Composite assignment operators.
In addition to the operators presented so far, C++ provides much more specific operations to change variables. Composite assignment operators combine operations of other operators, but cannot themselves be combined in statements with other operators.
For example, the +=
operator is used to sum two operands and assign them to the first operand.
While an expression can consist of a sequence of arbitrarily many operands and operators, an Composite assignment operator has only one operand as l-value and another operand as r-value. The l-value is assigned the result of the operation applied to the two operands.
Operator | Operation | |
---|---|---|
+= |
value1 = value1 + value2 | |
-= |
value1 = value1 - value2 | |
*= |
value1 = value1 * value2 | |
/= |
value1 = value1 / value2 | |
%= |
value1 = value1 % value2 | |
&= |
value1 = value1 & value2 | |
|= |
value1 = value1 | value2 |
^= |
value1 = value1 ^ value2 | |
<<= |
value1 = value1 « value2 | |
>>= |
value1 = value1 » value2 |
Similar to the increment or decrement operator, compound assignment operators do not add fundamentally new operations to the programming language. However, they are a convenient alternative for writing frequently occurring statements in a shorter and clearer way.
// listing1: compound assignment operators
#include <iostream>
#include <bitset>
int main() {
int operand1 = 7;
int operand2 = 4;
// +=
std::cout << "operand1: " << operand1 << " operand2: " << operand2 << std::endl;
std::cout << "operand1 = operand1 + operand2: " << (operand1 + operand2) << std::endl;
operand1 += operand2;
std::cout << " operand1 += operand2: " << operand1 << std::endl << std::endl;
// -=
std::cout << "operand1: " << operand1 << " operand2: " << operand2 << std::endl;
std::cout << "operand1 = operand1 - operand2: " << (operand1 - operand2) << std::endl;
operand1 -= operand2;
std::cout << "operand1 -= operand2: " << operand1 << std::endl << std::endl;
// *=
std::cout << "operand1: " << operand1 << " operand2: " << operand2 << std::endl;
std::cout << "operand1 = operand1 * operand2: " << (operand1 * operand2) << std::endl;
operand1 *= operand2;
std::cout << "operand1 *= operand2: " << operand1 << std::endl << std::endl;
// /=
std::cout << "operand1: " << operand1 << " operand2: " << operand2 << std::endl;
std::cout << "operand1 = operand1 / operand2: " << (operand1 / operand2) << std::endl;
operand1 /= operand2;
std::cout << " operand1 /= operand2: " << operand1 << std::endl << std::endl;
// %=
std::cout << "operand1: " << operand1 << " operand2: " << operand2 << std::endl;
std::cout << "operand1 = operand1 % operand2: " << (operand1 % operand2) << std::endl;
operand1 %= operand2;
std::cout << "operand1 %= operand2: " << operand1 << std::endl << std::endl;
// &=
std::cout << "operand1: " << std::bitset<8>(operand1) << " operand2: " << std::bitset<8>(operand2) << std::endl;
std::cout << "operand1 = operand1 & operand2: " << std::bitset<8>(operand1 & operand2) << std::endl;
operand1 &= operand2;
std::cout << "operand1 &= operand2: " << std::bitset<8>(operand1) << std::endl << std::endl;
// |=
std::cout << "operand1: " << std::bitset<8>(operand1) << " operand2: " << std::bitset<8>(operand2) << std::endl;
std::cout << "operand1 = operand1 | operand2: " << std::bitset<8>(operand1 | operand2) << std::endl;
operand1 |= operand2;
std::cout << "operand1 |= operand2: " << std::bitset<8>(operand1) << std::endl << std::endl;
operand1 = 5;
operand2 = 2;
// ^=
std::cout << "operand1: " << std::bitset<8>(operand1) << " operand2: " << std::bitset<8>(operand2) << std::endl;
std::cout << "operand1 = operand1 ^ operand2: " << std::bitset<8>(operand1 ^ operand2) << std::endl;
operand1 ^= operand2;
std::cout << "operand1 ^= operand2: " << std::bitset<8>(operand1) << std::endl << std::endl;
// <<=
std::cout << "operand1: " << std::bitset<8>(operand1) << " operand2: " << std::bitset<8>(operand2) << std::endl;
std::cout << "operand1 = operand1 << operand2: " << std::bitset<8>(operand1 << operand2) << std::endl;
operand1 <<= operand2;
std::cout << "operand1 <<= operand2: " << std::bitset<8>(operand1) << std::endl << std::endl;
// >>=
std::cout << "operand1: " << std::bitset<8>(operand1) << " operand2: " << std::bitset<8>(operand2) << std::endl;
std::cout << "operand1 = operand1 >> operand2: " << std::bitset<8>(operand1 >> operand2) << std::endl;
operand1 >>= operand2;
std::cout << "operand1 >>= operand2: " << std::bitset<8>(operand1) << std::endl << std::endl;
return 0;
}
Table of operators
We have learned about some operators in this and the last episode of the post series. For a better overview, I have summarized all of them in a table.
There we have built up a decent portfolio of operators. Now we are very well equipped for upcoming tasks.
At this point you can say, “That’s all I need to know! I know enough about operators now.” Nevertheless, I would like to give you some additional information about operators. You may not need them at the first moment, but in the course of the journey they will prove to be very useful.
Unary Operators
For completeness and so that you can do something with the term, I would like to briefly discuss Unary. Every now and then you come across this word in technical literature. The way it is used there, I imagined complicated definitions and spectacular functionality.
Maybe it was just me, but I had great respect for this technical term. But that was mainly due to my lack of knowledge. There is not much behind it.
A unary operator simply refers to an operand. Look at the negations operator !
. There it is very clear. !
takes the value of a boolean variable and reverses it. It refers only to this variable and does not consider any other operand.
That’s all there is to the term unary. We already have some unary operators in our toolbox.
operator | operation |
---|---|
+x |
unary plus |
-x |
unary minus |
++x |
increment |
--x |
Derement |
!x |
logical not |
~x |
complement |
sizeof | Determine size |
In other posts we have used sizeof
to determine the size in bytes of variables and data types. At first sight it seems to be a function, but in the C standard it is described as a unary operator. Only as a small theoretical fact at the edge.
You now know what an Unary is. To make the distinction to other operators even clearer, these have an analogous designation to it. If an operator has two operands, like for example the arithmetic multiplication operator *
, it is called Binary or binary operator.
As you can see, a lot of thought has gone into the development of programming languages and even the smallest details have received attention. And about the sensefulness of many details in expert circles is discussed fiercely.
Prefix or Postfix
All operators that are placed before the operands are called Prefix. For example, the negation !
is a prefix operator.
The Postfix operator is appended to the operand. We have learned about two postfix operators so far, but have used them as prefixes. I’m talking about the increment operator ++
and decrement operator --
.
So you can write either ++x
or x++
. But what is the point of this?
First we need to understand the differences.
The prefix works the way you probably think is natural. In the statement y = ++x;
, x
is incremented by 1
and then assigned to y
. The r-value is incremented and the l-value gets the incremented value.
The postfix behaves somewhat differently. There first the value of the r-value is assigned to the l-value and then the r-value is incremented. So the l-value keeps the old value of the r-value.
Let’s try this with a code example.
// listing2: prefix and postfix
#include <iostream>
int main()
{
int operand1 = 0;
int operand2 = 4;
std::cout << "operand1 = " << operand1 << " operand2 = " << operand2 << std::endl;
// prefix increment
operand1 = ++operand2;
std::cout << "after prefix increment" << std::endl;
std::cout << "operand1 = " << operand1 << " operand2 = " << operand2 << std::endl;
// prefix decrement
operand1 = --operand2;
std::cout << "after prefix decrement" << std::endl;
std::cout << "operand1 = " << operand1 << " operand2 = " << operand2 << std::endl;
// postfix increment
operand1 = operand2++;
std::cout << "after postfix increment" << std::endl;
std::cout << "operand1 = " << operand1 << " operand2 = " << operand2 << std::endl;
// postfix decrement
operand1 = operand2--;
std::cout << "after postfix decrement" << std::endl;
std::cout << "operand1 = " << operand1 << " operand2 = " << operand2 << std::endl;
return 0;
}
As you can see, it makes a huge difference to your instructions whether you choose prefix or postfix. It does not matter for the increment or decrement operations. The operand experiences the change at the same time in both variants. But the assignment takes place either at the postfix with the old value or at the prefix with the new value.
Another difference affects the performance of your program. While with the prefix the value is simply assigned after the operation, with a postfix operator a temporary copy of the old value must be made for the assignment.
This performance difference may sound very theoretical and not noticeable with integers, but it can become an argument with some classes or data types. Especially in low performance embedded systems.
Operator priorities
You surely agree with me that the basic rules of arithmetic from mathematics are known to everyone. Even if they are often used subconsciously. We adhere to calculation rules such as calculating the content of brackets first, powers have priority, dot calculation comes before line calculation, without having to think about it. And it goes without saying that we read from right to left.
We learned all this once. But does a computer follow the same rules?
Expressions are already evaluated from right to left. Assignments, however, are not. Here the right expression (r-value) is assigned to the left operand (l-value).
Are there any other deviations from the mathematical calculation rules? Or do operators even have completely different priorities?
Which value is assigned to the variable result
in listing3 after evaluation of the expression?
// listing3: operator precedence
#include <iostream>
int main()
{
int result = 11*3+20-5*5 << 1;
std::cout << "11*3+20-5*5 << 1 =" << result << std::endl;
result = 33+20-25 << 1;
std::cout << "33+20-25 << 1 =" << result << std::endl;
result = 28 << 1;
std::cout << "28 << 1 =" << result << std::endl;
result = (11*3)+20-(5*5) << 1;
std::cout << "(11*3)+20-(5*5) << 1 =" << result << std::endl;
return 0;
}
The result of the assignment is not random. The order in which the various operators are called is very precisely defined. And this is exactly what is meant by priority of the operators.
The execution priority and associativity of C++ operators are described in detail in the ISO standard .
Here a small excerpt, in order to be able to understand the evaluation of the expression in listing3.
rank | name | operator |
---|---|---|
2 | bracket | () |
5 | Multiply | * |
6 | Add | + |
6 | Subtract | - |
7 | Bitshift | < |
16 | Assign | = |
Operators with the highest rank are evaluated first. A sequence of operators with the same rank is processed from left to right.
With this knowledge look at the example from listing3 again. Now you can understand each step by the execution priority.
The highest rank in the expression 11*3+20-5*5 << 1
has the multiplication operators. This results in the intermediate values 33+20-25 << 1
. Then come the addition and subtraction. So it is continued with 28 << 1
. Then the bitshift follows before the final assignment and the expression stands for the value 56
.
As a little tip, I recommend you use parentheses. I like to use them to clarify priorities for myself and other programmers.
(11*3)+20-(5*5) << 1
This way I can more quickly see what value results from an expression. In addition, I am sure that my operation order is respected, because parentheses have a high rank. So you can increase the priorities of single operations.
That sticks
We have reached the end of the second part on operators in C++. First, the composite operators were still open. They combine different operators with an assignment operator. This way, statements can be made shorter and your code becomes clearer.
After that, we clearly listed all discussed operators from both parts in a table.
The first addition to operators was the explanation of Unary. This refers to operators that refer to only one operand.
Furthermore, we looked at what difference it makes to use the increment or decrement operator as prefix or postfix. We noticed that it primarily affects assignment. This is because with a postfix, the assignment is done first and then the operation is performed.
Finally, we compared mathematical calculation rules with the execution priorities of operators. While the priorities build on the arithmetic rules, they have little variations and idiosyncrasies here and there.
Knowing this, we can perform a lot of operations on our known data types. But with operators even the whole program flow can be designed. But more about that next time.
I wish you maximum success!
Sources
- [1] B. Stroustrup, A Tour of C++. Pearson Education, 2. Auflage, 29. Juni 2018.
- [2] B. Stroustrup, Programming: Principles and Practice Using C++. Addison Wesley, 2. Auflage, 15. Mai 2014.
- [3] U. Breymann, Der C++ Programmierer. C++ lernen – professionell anwenden – Lösungen nutzen. Aktuell zu C++17. München: Carl Hanser Verlag GmbH & Co. KG; 5. Auflage, 6. November 2017