Module 6: Expressions

  • what are logical operations?
  • what is the precendence of C++ operations?
  • what are type conversions?

Logical operations

A logical expression evalutes to a Boolean value: true or false. We use logical expressions in conditions for flow control (e.g., if statements and while loops). So far, our logical expressions have consisted of comparison operators:

Operator Operation
< Less than?
> Greater than?
<= Less than or equal to?
>= Greater than or equal to?
== Equal to?
!= Not equal to?

That’s all we’ve needed thus far in the course, but now we will start to write more complicated logical expressions. These will involve logical operations. Logical operations, just like arithmetic and comparison operations, operate on values and evaluate to a result. Some operate on two values (just like x + y): these are called binary operations. Others operate on a single value (just like -x): these are called unary operations. Logical operatons, like comparison operations, always evaluate to true or false. Unlike arithemetic or comparison operations, however, logical operations also operate on Boolean values.

We will concern ourselves with four logical operations in this course:

Keyword Operator Kind Evaluates to true iff:
and && Binary both operands are true
or || Binary at least one operand is true
xor ^ Binary exactly one operand is true
not ! Unary the operand is false

These operations mostly correspond that what you’d expect from colloquial use (in everyday speech). However, we can define them a bit more rigorously using a truth table. As an exercise, fill in the following truth table. Use the definitions of the various logical operators above.

a b a and b a or b a xor b not a
F F
F T
T F
T T

These operations will allow us to construct more interesting conditions like “$n$ is between 3 and 7”. Mathematically, we would write that expression as $3 \leq n \leq 7$, but 3 <= n <= 7 does not have the same meaning in C++. Let’s explore why below.

Precendence / order of operations

We have previously used an informal notion of precedence when evaluating expressions: “multiplication and division occur before addition and subtraction”. Now, when we add the logical operations into the mix of the kinds of things we can put in our expressions, things get a little bit more complicated. Here is a table that shows the precedence of all of the operations we now know about:

Operator Description
() sub-expression
!, +, - logical not, unary positive, unary negative
*, /, % multiplication, division, modulus
+, - addition, subtraction
<, <=, >, >= relational inequalities
==, != equal / not equal
and logical and
or logical or
= assignment

Compound operations

It is very common, in a variety of algorithms, to need to update a variable in-place by applying an arithmetic operation. For example, our printNumbersUpTo example included the statement i = i + 1, and our factorial example included number = number - 1. In C++ (and many other languages!), we can write these sorts of “apply an arithmetic operation and then save the result in the same variable” using compound operations. Here are a table of such operations:

Operation Equivalent to
x += y x = x + y
x -= y x = x - y
x *= y x = x * y
x /= y x = x / y

So, instead of writing x = x * 2, we can write x *= 2. Instead of i = i + 1, we can write i += 1. However, in fact, the need to increment (add one) and decrement (subtract one) is so common that they have their own operators. The ++ operator means, “add one to a variable and store the incremented value back in the variable” and -- means the same for subtraction.

Operation Equivalent to Also equivalent to
i++ i += 1 i = i + 1
i-- i -= 1 i = i - 1

The ++ and -- operators can be applied either before or after a variable (e.g., i++ or ++i). In both cases, the value will be incremented by one. The only difference between them is what the whole expression will evaluate to: the old value of the variable or the new value. This distinction will not matter in code I show you in ENGI 1020. In fact, code that relies on the distinction between i++ and ++i may be a bit too clever for its own good: this kind of tricky code is also tricky to understand and tricky to debug.

Numeric conversions

What does the following code output?

int n = 4.6 * 2;
double x = 7 / 2;

cout << "n: " << n << ", x: " << x << endl;

In the first case, we are multiplying a floating-point number by an integer, then storing the result in an integer variable. The computer cannot do this directly. In the second case, we are dividing an integer by an integer and then storing the result in a floating-point variable. The result of this initialization might surprise you.

When we perform an operation on integer and floating-point numbers, we must convert one to the other. In C++ (and many, but not all programming languages), the compiler will automatically convert an integer to a floating-point number when it’s involved in a floating-point operation (addition, subtraction, etc.). This numeric promotion is safe in the sense that no information is lost: it’s safe to refer to 3 as 3.0, etc. The same kind of promotion occurs when we store integer values in floating-point variables (including parameters!).

In the other direction, we can also assign floating-point values to integer variable (including parameters). When this happens in many — but not all — programming languages, the floating-point value is truncated: anything after the decimal place is simply chopped off. Even though 4.9 is almost 5, it will be truncated to 4 if stored in an integer variable.

Boolean conversions

In most programming languages, numbers can be converted to Boolean values and vice-versa. Boolean values are converted to integer types as follows:

  1. true becomes 1 and
  2. false becomes 0.

Integer types include int, long int and even char (which, after all, is an integer code for a character). The above conversion might be about what you expect, but you may find the reverse to be slightly surprising:

  1. 0 becomes false and
  2. anything else becomes true.

These conversions are often applied automatically. For example, the following code will compile and run just fine:

int i = getNumberFromUser();
if (i)
{
	cout << "i is not zero!\n";
}

The short-circuit property

When evaluating a boolean condition, we follow the order of operations to reduce the condition down to a single true or false value. However, sometimes we don’t need to evaluate the whole condition to know the answer! Consider the following code:

bool sleptWell = false;
bool haveCoffee = true;

if (haveCoffee or sleptWell)
{
	giveLecture();
}

This condition will be true if either haveCoffee or sleptWell are true. Consequently, as soon as we see that haveCoffee is true, there is no need to continue checking the rest of the condition (sleptWell): we already know what the answer is going to be. This is the short-circuit property: as soon as we know what a condition will evaluate to, we can stop evaluating it. This may not seem terribly exciting when checking boolean variables, but consider a different case:

bool foo(double x) { return (x > 0); }
bool bar(double x) { cout << x; return (x > 1); }

int main()
{
	if (foo(2) or bar(2))
	{
		cout << "wibble";
	}
	return 0;
}

In this case, the behaviour of the program is changed by the short-circuit property: the bar function will never run. This is worth understanding when we write conditions for if statements and loops!

Exercises

Boolean algebra

Given four integer variables, A, B, C and D, write four C++ functions functions to return true iff:

  1. any of them is negative,
  2. A is the smallest value,
  3. any pair is equal and
  4. the product of any pair is equal to the product of the other pair.

Rounding

By default, the C++ compiler will truncate floating-point numbers when storing them in integers, i.e., chop off anything after the decimal point. The standard library does include several rounding functions, but let’s write one for practice. Write a function that rounds a floating-point number to the nearest integer. It would be advisable to design before implementing: create the contract of the function and think about all of its logic, the special cases it needs to handle, how to test it, etc., before you write any code.

License: CC BY-NC-SA

(c) 2009–2016 Michael Bruce-Lockhart, Theo Norvell, Dennis Peters and Jonathan Anderson. Licensed under a Creative Commons Attribution–Noncommercial–Share-Alike 2.5 Canada License. Permissions beyond the scope of this license may be available at theteachingmachine.org.