In many cases, we are posed with a problem, where based on some condition, we may have to take one action or another. One famous cartoon is
We start by answering a yes-no question, and based on that response, we then proceed to a second yes-no question. This second response then indicates the appropriate course of action to take.
For a flow diagram, we have two possible circumstances:
These are shown in the next two figures, respectively.
Figure 1. Performing operations if the condition is true.
Figure 2. Performing certain operations if the condition is true
and other operations if the condition is false.
Suppose, for example, we want to define the absolute value function. This function takes a real value $x$ and returns $x$ if $x \ge 0$ and $-x$ if $x < 0$. A flow cart for the absolute value function would be as shown in Figure 3 while Figure 4 shows a plot of this function.
Figure 3. A flow chart for the absolute value function.
Figure 4. A plot of the absolute value function.
To date, the only Boolean-valued operators we have seen (operators that return true or false) are the comparison operators ==, !=, <=, <, > and >=. We would use the condition x >= 0. In order to program such a flow, we must however have a means of saying if something is true, do something, otherwise, do something else. For this, we have the conditional statement:
if ( Boolean-valued condition ) { // The 'consequent block of statements' or 'consequent body' // - statements to be executed if the condition is true } else { // The 'alternative block of statements' or 'alternative body' // - statements to be executed if the condition is false }
This entire structure is referred to as a conditional statement even if the consequent and alternative bodies have one or more statements within them.
We can now use this and the comparison operator to implement the absolute value function. We will have to use an identifier that uses the alphabet, so we will use abs:
// Function declarations double abs( double x ); // Function definitions double abs( double x ) { if ( x >= 0 ) { return x; } else { return -x; } }
Recall that each operator has a complementary operator, so if x >= 0 is true, then x < 0 must be false. If we use the complementary operator, we must swap the consequent and alternative bodies:
int abs( int x ) { if ( x < 0 ) { return -x; } else { return x; } }
Next, let us implement the max function:
$\max(x, y) = \left\{ \begin{matrix} x & x\geq y\\ y & x<y \end{matrix} \right.$.You will note that the two conditions are complementary, so this may be implemented as a single conditional statement:
double max( double x, double y ) { if ( x >= y ) { return x; } else { return y; } }
In each of these cases, one or more arguments are passed to these functions, and based on the value of these arguments, specific code is executed.
Let us now look at another function: a clipped signal. Given a maximum absolute signal bound $b$, ${\rm clip}(x, b) = \left\{ \begin{matrix} -b & x < -b \\ x & -b \le x \le b \\ b & x > b \end{matrix} \right.$.
First, we must convert this into conditional statements:
We can draw this as a flow chart:
We can now implement this:
double clip( double x, double b ) { if ( x > b ) { return b; } else { if ( x < -b ) { return -b; } else { return y; } } }
Notice that the alternative body is a single conditional statement. In this case, we can beautify the code by rewriting it as follows:
double clip( double x, double b ) { if ( x > b ) { return b; } else if ( x < -b ) { return -b; } else { return y; } }
This can be drawn as an alternative flow chart as shown in Figure 5.
Figure 5. An if—else if—... sequence of statements.
If the first condition is true, the first consequent body is executed, otherwise if the second condition is true, the second consequent body is executed, otherwise the alternative body is executed.
This is also described as a cascading conditional statement, as it suggests a waterfall with multiple steps, as shown in Figure 6.
Figure 6. The cascading Song Khon Waterfall by Martin PĆ¼schel.
As another function that can be described by a cascading conditional statement, consider the tent function:
${\rm tent}(x) = \left\{ \begin{matrix} 0 & x \le -1 \\ x + 1 & -1 < x \le 0 \\ 1 - x & 0 < x \le 1 \\ 0 & x > 1 \end{matrix} \right.$.
While there are two conditions for the two intermediate cases, if we have already executed the first consequent body because $x \le -1$, then for the second condition, we do not have to test both $x > -1$ and $x \le 0$, but rather we only need to test if $x \le 0$, and similar for the third case. Thus, this function may be implemented as:
double tent( double x ) { if ( x <= -1.0 ) { return 0.0; } else if ( x <= 0.0 ) { return x + 1.0; } else if ( x <= 1.0 ) { return 1.0 - x; } else { return 0.0; } }
The flow chart would have three conditions, as shown in Figure 7.
Figure 7. A cascading conditional statement with three conditions.
Note that we could have implemented the tent function in two different ways if we used the abs function:
double tent_3_cases( double x ) { if ( abs( x ) >= 1.0 ) { return 0.0; } else if ( x <= 0 ) { return x + 1; } else { return 1 - x; } } double tent_2_cases( double x ) { if ( abs( x ) >= 1.0 ) { return 0.0; } else { return 1 - abs( x ); } }
Note that we are not reducing the number of conditional statements that are being executed, because the abs function itself contains a conditional statement.
Novice programmers will often try to emphasize the condition of the alternative body, and so rather that writing the absolute value function as above, they will write it as:
// Function definitions double abs( double x ) { if ( x >= 0 ) { return x; } else if ( x < 0 ) { return -x; } }
The novice believes this is clearer, but it actually confuses experienced programmers. If you want to make sure that the condition is satisfied for the alternative body, you can use something called an assertion:
// Pre-processor include directives #include <cassert> // Function definitions double abs( double x ) { if ( x >= 0 ) { return x; } else { assert( x < 0 ); return -x; } }
Now, when the alternative body is executed, it will first evaluate x < 0 and if this returns false, the program will terminate.
Two questions: What do you think the programmer meant by the following function, and what do you think actually happens when you call this function?
double square_wave( double x ) { if ( x <= -1.0 ) { return 0.0; } else if ( x < 0.0 ) { return -1.0; } else if ( x > 0.0 ) { return 1.0; } else if ( x >= 1.0 ) { return 0.0; } else { assert ( 0.0 == x ); return 0.0; } }
How would you correct this?
The cardinal sine function or sinc function is defined as
${\rm sinc}(x) = \left\{ \begin{matrix} 1 & x = 0 \\ \frac{\sin(\pi x)}{\pi x} & x \ne 0 \end{matrix} \right.$.
In this function, we have a very special case when $x = 0$, and the general case is when $x \ne 0$. If we are dealing with special cases, it is often easier to put those conditions at the front.
// Pre-processor include directives #include <cmath> // Function declarations double sinc( double x ); // Function definitions double sinc( double x ) { // The special case when x = 0: if ( x == 0.0 ) { return 1.0; } // The general case return std::sin( 3.1415926535897932*x )/ (3.1415926535897932*x); }
We are not restricted to using comparison operators in as conditions in conditional statements. Another choice is to use functions that return a Boolean value. Suppose, for example, we write the following function:
// Function declarations bool is_divisible( int m, int n ); // Function definitions // Return 'true' if m/n leaves no remainder, 'false' otherwise. // - that is, is 'm' an integer multiple of 'n' bool is_divisible( int m, int n ) { return ((m % n) == 0); }
We can then use this in another function where the conditional is a call to this function:
// Function declarations int factor_out_bad_luck_once( int n ); // Function definitions int factor_out_bad_luck_once( int n ) { if ( is_divisible( n, 13 ) ) { return n/13; } else { return n; } }
While this is an obviously concocted example, it at least shows a point.
Recall our implementation of the fast sine function:
double fast_sin( double x ) { return (( -0.11073981636184074*x - 0.057385341027109429 )*x + 1.0)*x; }
This only returned a reasonable answer if $0 \le x \le \frac{\pi}{2}$. Suppose however, we have a value of $x$ that satisfied $-\frac{\pi}{2} \le x < 0$. If we tried to call this function with such a value of $x$, we would get the wrong answer. We could however, remember that $\sin(-x) = -sin(x)$ or equivalently $\sin(x) = -\sin(-x)$, so calculating $\sin(-0.3)$ can be done by calculating $-\sin(0.3)$.
Your first thought may be to do the following:
double fast_sin( double x ) { if ( x >= 0 ) { return (( -0.11073981636184074*x - 0.057385341027109429 )*x + 1.0)*x; } else { assert( x < 0 ); return -(( -0.11073981636184074*(-x) - 0.057385341027109429 )*(-x) + 1.0)*(-x); } }
This is, however, a little unsatisfying. An alternate approach is to author the function as follows:
double fast_sin( double x ) { if ( x < 0 ) { return -fast_sin( -x ); } assert( x >= 0.0 ); return (( -0.11073981636184074*x - 0.057385341027109429 )*x + 1.0)*x; }
Let's see what happens if we call fast_sin( -0.3 ):
We will have many opportunities to call recursive functions throughout this course.
1. Write a function median_of_three that takes three arguments and returns the median of those three.
double median_of_three( double a, double b, double c ) { // Enter your implementation here }
2. Under the rules of the calendar in general use in Canada, a year is a leap year under the following conditions:
Write a function to take an argument and determine whether or not it is . Treat negative numbers as years BCE, and ignore the fact that technically 0 is not a year, just return false.
bool is_leap_year( int n );
Test your function by ensuring that 1200, 1500, 1600 and 2000 are leap years but 1700, 1800 and 1900 are not leap years. Also, test your code for a few other years to make sure you are return the correct value.
3. The normalized cardinal sine function, defined as $sinc(x) = \frac{\sin(\pi x)}{\pi x}$ for $x \ne 0$ and $1$ if $x = 0$ is a very common is an incredibly useful function for signal processing in all fields of engineering. The problem is, we have a singularity in the formula when $x = 0$, so we cannot implement the function as
double sinc( double x ) { return std::sin( 3.1415926535897932*x )/(3.1415926535897932*x); }
In general, it's often a bad idea to evaluate formulas even close to a singularity, so modify the above function so that:
Test your function by calculating sinc(0.0001), which should execute the second case, and sinc(0.0000999999999999999), which should execute the first case. Are these values close?
Note: in your first-year calculus course, you will learn about Taylor series, which will explain where the above approximation comes from.
4. Write a function that implements the unit step function, defined as $u(x) = \left\{ \begin{matrix} 1 & x\geq 0\\ 0 & x<0 \end{matrix} \right.$.
5. Write a function that implements the maximum of three function. You will start with one condition that compares the first two parameters, but what goes into the consequent and alternative bodies?
// Function declarations double max( double x, double y, double z ); // Function definitions double max( double x, double y, double z ) { if ( x >= y ) { // Do something assuming x >= y } else { // Do something assuming y > x } }
Rather than jumping to start programming, you should:
6. Write a function that approximates an impulse function as follows:
The name of this function should be approx_impulse.
7. For Question 6, modify it as follows:
These two special cases should be in a conditional statement separate from the general case when $d > 0$.
8. The operational amplifier is very similar to the clipping function that we described earlier, but with slightly different properties. The operational amplifier is associated with an input signal $x$, a gain $g$ that is assumed to be greater than $0$, and two voltages $v_{\rm min}$ and $v_{\rm max}$.
The function declaration should be:
// Function declarations double op_amp( double x, double gain, double v_min, double v_max );
9. Modify the fast sine function by observing that if $\frac{\pi}{2} < x \le \pi$ then $\sin(x) = \sin(\pi - x)$ and where $0 \le \pi - x < \frac{\pi}{2}$ which falls in our acceptable range.
You may use the approximation of $\frac{\pi}{2}$ by 1.5707963267948966.
10. Modify the fast sine function in Question 9 by observing that if $\pi < x \le 2\pi$ then $\sin(x) = -\sin(2\pi - x)$ and where $0 \le 2\pi - x < \pi$ which falls in our condition for Question 9.
You may use the approximation of $\pi$ by 3.1415926535897932.
11. Modify the fast sine function in Question 10 by observing that if $x > 2\pi$ that $\sin(x) = \sin(x - 2\pi)$.
You may use the approximation of $2\pi$ by 6.2831853071795865.
At this point, you now should have a sine function that can return an approximation of $\sin(x)$ for any real value of $x$, even $x = -100$.