2.2. Common Operators

Time: 00:06:13 | Download: Large, Large (CC) Small | Streaming, Streaming (CC) | Slides (PDF)

Looking at the table at the end of the previous section might be intimidating: C++ seems to have many (often bewildering) operators. Fortunately, we can do a great deal of programming with just a few that are familiar or at least intuitive. However, we must not let familiarity and intuition mislead our quest to understand these fundamental programming operators.

Assignment Operator

The assignment operator, =, is straightforward, but does not have the same meaning or behave in the same way as the equals symbol used in mathematics.

x = y + 5 x = y + 5;
(a)(b)
Algebraic equals vs. C++ assignment.
  1. In mathematics, = means that for some values of x and y, the left and right hand sides of = represent the same numeric value. Alternatively, it means that it is possible to substitute y + 5 for x wherever x appears in an equation.
  2. In C++, = is a binary operator where the right-hand side is an arbitrarily complex expression and the left-hand side is a variable. So, the C++ statement translates to, Add 5 to the current value stored in y and then store the result in x.

Recall from Chapter 1 that variables have three characteristics: a name, a content, and an address. Now, focus on two parts of the description of the assignment operation in the previous figure: "Add 5 to the current value stored in y and then store the result in x." The first part uses the contents of y, while the second part uses the address of x. We can use these characteristics to help us visualize what actions take place as the program uses variables.

x = y + 5;
The picture illustrates the evaluation of x=y+5. Assume that y contains 3: the compiler generates code to load 3 from y, adds the constant 5 to it, and stores the result, 8, into the memory location named x.
Using variables in statements. When the program uses a variable in a statement, the compiler must determine if its name represents its contents or address in memory. The compiler follows the general rule: The arithmetic operators have higher precedence than the assignment operator, so the computer evaluates the expression on the right side of the assignment operator before the assignment operation occurs.

The discussion of associativity in the previous section suggested that the assignment operator produces a value, allowing programmers to chain multiple assignment operations in a single statement. Assignment chaining obscures the meanings of l- and r-values. Chapter 3 briefly describes the C++ syntax rules allowing the interchange of statements and expressions.

int z = x = y + 5;
The picture illustrates the evaluation of z=x=y+5. x=y+5 is evaluated as in the previous figure, and then result, 8, is stored in z.
Assignment chaining. The assignment operator is right-associative, so expressions on the right are evaluated first, and evaluation continues right to left. In this example, the right assignment operator uses x as an r-value, while the left assignment uses it as an l-value. The program evaluates the expression y + 5 and stores the result, 8, in x. The next assignment operator (to the left) extracts 8 from x and stores it in z. Assignment chaining is rare but is sometimes helpful when initializing or resetting multiple variables to the same value: x = y = z = 0;

Arithmetic Operators

Most remaining arithmetic operators are likely familiar to most programmers. Nevertheless, two of them, plus the less familiar modular operator, may exhibit some unfamiliar behaviors, warranting a brief examination.

+ Addition
- Subtraction
* Multiplication Mathematics denotes multiplication with two adjacent variables: ab. However, the C++ compiler interprets the sequence as a single variable. Multiplying two variables in a C++ program always requires the * operator.
/ Division Division behaves as expected unless both operands are integers. Division is detailed next.
% Modular The modular or remainder is likely the least familiar, so it is detailed below.
Arithmetic operator summary. The first three operators behave quite predictably, but division's behavior will likely surprise some, and some will not be familiar with the modular operation. The operand data types determine the result type: C++ does not have an exponentiation operator: a^b. It does use the symbol but as the bitwise exclusive-OR operator, described in the supplemental section at the end of the chapter.

Division Operator Details

Sometimes, it's difficult to differentiate the effects of the division and output operations. The C++ fundamental, built-in, primitive data types table in Chapter 1 lists various kinds of integers and floating-point types and their respective ranges. While C++ displays floating-point numbers with six significant digits of accuracy by default, it internally maintains the value to the full accuracy of the specific data type. We'll explore output operations in greater detail in the next chapter.

Floating Point Floating Point & Integer Integer
10.0 / 2.0 → 5.000000 10.0 / 2 → 5.000000 10 / 2 → 5
1.0 / 3.0 → 0.333333 1 / 3.0 → 0.333333 1 / 3 → 0 (truncates)
5.0 / 9.0 → 0.555556 5 / 9.0 → 0.555556 5 / 9 → 0 (truncates)
9 / 10 → 0.900000 9.0 / 10 → 0.900000 9 / 10 → 0 (truncates)
Division outcome by operand type. Although familiar, division's behavior often surprises new programmers. If one or both of its operands are a floating-point type (e.g., float or double), it behaves as expected. However, suppose both operands are integers. In that case, the program carries out the operation using integer arithmetic, AND the result is an integer (i.e., NO decimal or fractional amount). The division operator truncates rather than rounds when it discards the fractional part. This behavior is often called integer division.

Typically, at least one operand is a variable or a more complex expression. In this situation, there is a good chance that the division operation will lead to a loss of precision, that is, a loss of information. For example, assuming that score stores the value 95 and we divide by 10: score / 10 = 95 / 10 = 9. But is the precision loss an error? Oddly, truncation is sometimes very helpful (see money.cpp later in this chapter for a simple example). But sometimes, it is a truncation error. Error or not depends on the programmer's intentions and the program's needs.

Modular Operator Details

The modular operator (also known as the remainder operator) might not be familiar to some, but it is listed with the "common" operators because computer scientists use it often. The easiest way to understand how the modular operator works is by remembering how we first learned to divide. When the divisor does not evenly divide the dividend, we expressed the result as a quotient with a remainder. The modular operator returns the remainder.

A three-part picture illustrates the modular operator in the expression 11%4. Part (a) shows 11 smiley faces. Part (b) shows the smiley faces grouped into 2 groups of 4. Part (c) shows that 3 smiley faces do not fit into a group of 4 (i.e., they remain). Therefore, 11%4 is 3.
The modular (remainder) operator illustrated. The figure illustrates the meaning of 11 % 4 = 3:
  1. Begin with 11 items
  2. Group the 11 items into 2 groups of 4 (equivalent to integer division or 11 / 4 = 2)
  3. 3 items do not fit into a group of 4; they are leftover or remain
When m > 0 and n > 0, then 0 ≤  m % n < n - 1 (that is, the result is always in the range [0 - n-1] ). Although m and n are typically positive (in the kind of problems computer scientists often encounter), the mod operator is defined on negative values as well. This example illustrates the impact of negative operands on the results of the mod operator.

Converting Data Types

Through our study of programming in general and C++ in particular, we understand that data types profoundly impact a program's behavior. Furthermore, sometimes, it is necessary to change the data type of an expression to achieve the behavior necessary to solve a particular problem. Sometimes, the compiler will automatically generate the code to perform the conversion, while other times, programmers must explicitly do the conversions themselves.

Type Promotions

The compiler can automatically convert a "narrow" data type to a "wider" one. In this context, "narrow" and "wide" refer to the dynamic range of values stored rather than the number of bytes of memory each type requires. For example, both long and float use four bytes of storage, but float is the widest because it can store larger and smaller (between 0 and 1) numbers than long. An automatic type conversion from narrow to wide is called a type promotion. Evaluating an operation where the operands have different types triggers an automatic type promotion, which we can sometimes use to solve the truncation problem presented above.

int score;
    ...
score / 10.0;
double score;
    ...
score / 10;
(a)(b)
Type promotion examples. Each data type's internal representation or bit pattern is distinct, making it impossible for an operator to act on operands whose types are different. So, the compiler automatically promotes the narrowest type to the widest.
  1. The compiler interprets 10.0 as type double (see Numeric Data Type Rules), and promotes score to a double to complete the division operation.
  2. score is a double, which forces the compiler to promote 10 to a double before it is able to complete the division operation.

In both examples, type promotion can solve the truncation problem. But what happens when both operands are integer variables? This situation forces programmers to use an explicit typecast (described below).

Additionally, the C++ compiler also generates code to automatically convert char to int and int to char. The compiler does this because many library functions work with characters but have parameters and return types that are integers (see, for example, Character Testing and Conversion Functions). There are at least two reasons that C++ works with characters as integers. Most importantly, type int is deliberately the same size as the computer's native word size, which the computer can transfer to and from memory with the greatest speed. Of lesser importance is that C++ defines an integer symbolic constant named EOF (end of file). Some functions that read files one character at a time return this value to signal that the read operation has reached the end of the file (see wc.cpp).

Typecasting

Casting is not an operation that we use in our everyday lives. Nevertheless, I include it with the "common" operators because it helps solve problems with integer division. Java only supports one casting notation, but C++ supports two. The operator notation, recognized both by languages, is formed by enclosing the new or destination data type in parentheses and placing it before an expression, that is, before the value to convert. The precedence of the casting operator is relatively high, so programmers must group with parentheses complex expressions involving other operators. The functional notation looks like a function call where the name of the new or destination type is the function name, and the single argument is the expression whose value the cast will convert.

Operator NotationFunctional Notation
(double)score / count
score / (double)count
(double)score / (double)count
double(score) / count
score / double(count)
double(score) / double(count)
(a) - C-style cast
(int)(3.14 + 2.7)
int(3.14 + 2.7)
(b) - C-style cast
(double)(score / count)
double(score / count)
(c) - function-style cast
Typecasting examples. The examples illustrate two typecasting notations. In all the examples, score and count are integer variables.
  1. Solving the truncation problem: Programmers may cast one or both operands. If they cast one operand, the program automatically promotes the other. After the type conversions, the division operation completes using floating-point arithmetic, avoiding truncation errors.
  2. Expression evaluation first. The expression is evaluated before the typecast occurs in both examples. The complete expression (the addition and the typecast) is integer-valued and has the value 5.
  3. An error! The expression score / count is evaluated before the typecast tasks place. The typecast notwithstanding, both versions create a truncation error.
Note that a type conversion forms an expression whose type is changed, but the cast or the promotion alters neither variable.

Modern C++ Type Conversions

The ANSI C++ standard regarding type conversions has evolved recently. Previously, the standard required the compiler to perform an explicit typecast to convert from a wide type to a narrower type, i.e., from double to int. The wide-to-narrow conversion could result in a loss of precision, e.g., changing 10.5 to 10 would lose the fractional part of the data. In the past, the compiler made programmers "take responsibility" for the potential loss with an explicit cast operation, but newer compilers will perform the type conversion automatically.

#include <iostream>
using namespace std;

int main()
{
	int i = (int)(3.14 + 2.7);
	cout << i << endl;
	return 0;
}
#include <iostream>
using namespace std;

int main()
{
	int i = 3.14 + 2.7;
	cout << i << endl;
	return 0;
}
(a)(b)
Automatic C++ type conversions. The current ANSI C++ standard relaxes the older requirement to cast wide data to narrow before using it as the narrow type. So, modern C++ compilers now automatically convert wide data to a narrower type, double to int in this example. The automatic conversion takes place for most operators.
  1. Previously, the explicit typecast (highlighted in yellow) was needed to avoid a compiler error.
  2. The addition operation, 3.14 + 2.7, is a double-valued expression - the result is type double. Modern C++ compilers automatically convert the sum to type int to complete the assignment operation.
Both versions of the program print 5 to the console.

While the newer standard makes some programming operations easier, it also places a greater responsibility on programmers to detect and correct truncation errors. Recall that when the compiler generates code to evaluate a division operation using integer arithmetic, it always truncates the results - it discards the fraction - rather than rounding the value to the nearest integer. Inappropriately converting a wide type to a narrow type is one cause of truncation errors in a program.

double average(...)
{
	int sum = 0;
	int count = 0;

	. . .

	return sum / count;
}
A truncation error example. Imagine we need to calculate the average of a series of integer values. The partial function (ellipses represent code removed for simplicity) illustrated here is our first attempt. The function's double return type notwithstanding, the function still exhibits a truncation error. Both sum and count are integers, so the computer completes the division operation using integer arithmetic, producing an int value. After the program completes the division operation, it promotes the average back to a double, completing the return operation.

The highlighted division operation causes a truncation error. Compilers written to the older ANSI standard would detect and warn programmers about this error, but newer compilers carry out the type conversions automatically, concealing the error. Compare this code to Figure 5 above. We will revisit this problem/example in Chapters 3 (Practice Problem 2) and 5 (Practice Problems 4-6).

Limits Of Casting

If you look carefully at the preceding examples, you will notice that they all involve numeric data types; they are all different kinds of numbers. I chose int and double because they are easy to understand and we have already seen the numeric data type rules that govern how the compiler treats numeric constants. But the examples should make us wonder if casting is possible with other data types.

Informal Casting Rule

To cast from one data type to another, the two data types, the new and the current types, must be "sort of the same" to begin with.

"Sort of the same" is too ambiguous for an exact discipline like computer programming, so let's begin refining the phrase with an example. In mathematics, there is little difference between 2 and 2.0, but in the memory of a running C++ program, the bit-patterns are quite different. The difference implies that sometimes we need to convert one type to the other, but it is the similarity, an int and a double are "sort of the same," that makes the conversion meaningful. They are different kinds of numbers with different representations in memory, but numbers nevertheless. A typecast changes the representation of the number in memory without changing the fundamental concept that it is a number.

Armed with this understanding, we are ready to explore some relatively simple type conversions that we cannot do with a cast. For example, suppose we want to convert a number to a string or a string to a number. A string and a number aren't "sort of the same:" one is a string or sequence of characters, and the other is a single numeric value. The two are too dissimilar to convert between them with a typecast. Yet, the conversion is necessary whenever a program reads numbers from or writes numbers to the console. We'll explore functions that perform the conversions in Chapter 8.

Sometimes, we can even cast an object from one class type to another. This cast will only work if the two classes are related by inheritance (formally introduced in Chapter 10). For example, a program might have a class named Car and another class named Convertible. If the classes are related by inheritance, we can say, "A Convertible is a Car," and we can cast a Convertible to a Car.

But it's not always possible to cast one class to another. Imagine that a program has two classes, Person and Circle. What would it mean to try to convert a Person to a Circle or a Circle to a Person? The two are not "sort of the same" they are fundamentally different kinds of things and it doesn't make sense to try to convert one to the other.