Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

from __init__ import *

Motivation

It is time to learn to compose a program from its ingredients. Imperative programming is a programming paradigm that uses the flow of execution of statements, also known as the control flow, to change a program’s state for the desire computation.

GCD

To motivate the concept, recall the program for computing the GCD in Lecture 2:

{
  b = b % a;
  int c = b;
  b = a;
  a = c;
}

where a and b are the non-zero integer input as follows:

%%cpp
int a=2*3*4, b=3*4*5;
cout << format("gcd({}, {})=?\n", a, b);
gcd(24, 60)=?

Using control flow statements, we can improve the program to conditionally stop calculating the GCD when it is obtained as the absolute value of b, i.e., when a becomes 0:

1
2
3
4
5
6
if (a) { // execute if a is non-zero
  b = b % a;
  int c = b;
  b = a;
  a = c;
} else if (b<0) b=-b; // get the absolute value of `b` as the GCD

Program 1:Conditionally compute the GCD of a and b.

See if you can crash it by running the program too many times!

%%cpp
if (a) {
    b = b % a;
    int c = b;
    b = a;
    a = c;
    cout << format("gcd({}, {})\n", a, b); // intermediate answer
} else if (b<0) b=-b;
else cout << b << "\n"; // final answer
12

We can also free ourselves from repeating the execution manually by writing an iteration or a loop using control flow statements.

1
2
3
4
5
6
7
8
int c;
while (a) {  // execute repeatedly as long as a is non-zero
    b = b % a;
    c = b;
    b = a;
    a = c;
}
if (b<0) b=-b;

Program 2:Iterative computation of the GCD of a and b.

The program returns the GCD in one go:

%%cpp
int a=2*3*4, b=3*4*5; // input
cout << format("gcd({}, {})=", a, b);

int c;
while (a) {  // execute repeatedly as long as a is non-zero
    b = b % a;
    c = b;
    b = a;
    a = c;
}
if (b<0) b=-b;
cout << b << "\n"; // final answer
gcd(24, 60)=12

Inverse Square Root

Recall the update rule of the inverse square root in Lecture 2:

y = y * (1.5 - (x*0.5 * y * y));

where x is the input, and y is the initial guess:

%%cpp
double x = 10./3;  // input
cout << format("rsqrt({})=?\n", x);

auto i = *reinterpret_cast<int64_t *>(&x);
i = 0x5fe6eb50c7b537a9 - (i >> 1);
auto y = *reinterpret_cast<double *>(&i);
cout << format("Initial guess: {}\n", y);  // initial guess
rsqrt(3.3333333333333335)=?
Initial guess: 0.5495583757284046

We can improve the program to check whether further execution is required:

1
2
3
4
5
6
7
8
9
y = (y+1/y/x)/2;   // update the answer

if (auto x_=1/y/y, 
    gap=(x>x_)?(x-x_):(x_-x), 
    abs_x=(x>0?x:-x), 
    abs_x_=(x_>0?x_:-x_); 
    y<0 || x_!=x && gap > 1e-9*(abs_x>abs_x_?abs_x:abs_x_)) { // inaccurate?
    cout << format("Run again to improve the accuracy.");
}

Program 3:Conditional update of a guess y of the inverse square root of x.

Observe that the program prompts the user to run it again when the answer is inaccurate:

%%cpp
y = y * (1.5 - (x*0.5 * y * y));

if (auto x_=1/y/y, 
    gap=(x>x_)?(x-x_):(x_-x), 
    abs_x=(x>0?x:-x), 
    abs_x_=(x_>0?x_:-x_); 
    y<0 || x_!=x && gap > 1e-9*(abs_x>abs_x_?abs_x:abs_x_)) {
    cout << format("An improved answer: {}\n", y);
    cout << format("Run again to improve the accuracy.");
} else cout << format("The final answer: {}.\n", y);
The final answer: 0.547722557271348.

The repeated runs can be automated and optimized with the following code:

1
2
3
4
5
6
7
constexpr auto threehalfs = 1.5, rel_tol = 1e-9; // for compiler optimization
const auto x2 = x*0.5;                           // avoid redundant calculations
double x_, gap, abs_x, abs_x_;
do {
    y = y * (threehalfs - (x2 * y * y));
    x_=1/y/y; gap=(x>x_)?(x-x_):(x_-x); abs_x=(x>0?x:-x); abs_x_=(x_>0?x_:-x_);
} while (y<0 || x_!=x && gap > rel_tol*(abs_x>abs_x_?abs_x:abs_x_)); // repeat if inaccurate

Program 4:Iterative update of a guess y of the inverse square root of x.

This gives an accurate answer immediately:

%%cpp
double x = 10./3;  // input

auto i = *reinterpret_cast<int64_t *>(&x);
i = 0x5fe6eb50c7b537a9 - (i >> 1);
auto y = *reinterpret_cast<double *>(&i);

constexpr auto threehalfs = 1.5, rel_tol = 1e-9;
const auto x2 = x*0.5;
double x_, gap, abs_x, abs_x_;
do {
    y = y * (threehalfs - (x2 * y * y));
    x_=1/y/y; gap=(x>x_)?(x-x_):(x_-x); abs_x=(x>0?x:-x); abs_x_=(x_>0?x_:-x_);
} while (y<0 || x_!=x && gap > rel_tol*(abs_x>abs_x_?abs_x:abs_x_));
cout << format("rsqrt({})={}.\n", x, y); // final answer
rsqrt(3.3333333333333335)=0.547722557271348.

Undecimal to decimal

Consider a new problem of converting an undecimal number to a decimal number.

The following is an implementation that converts an undecimal number represented by a C string to an unsigned integer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
auto value=0uLL;
constexpr auto m1=-1uL, m2=m1/11, m3=m1-10;
for (size_t i=0, length=strlen(s); i < length; ++i) {
    value *= value<=m2 ? 11 : throw runtime_error("The value is too big.");
    switch (unsigned char c=s[i]) {   // condition in the form of a character declaration
    case 'X':                // case label X
        [[fallthrough]];     // attribute to silent warning
    case 'x':
        value<=m3 ? value+=10 : throw runtime_error("The value is too big.");
        break;               // terminate the switch statement
    default:                 // default label
        if ((c=s[i]-'0')>9) throw runtime_error("Invalid character found.");
        value<=m1-c? value+=c : throw runtime_error("The value is too big.");
    }
}

Program 5:Computing the integer value of an undecimal C string s.

Control flow statements allow the program to handle undecimal strings of any length, limited only by the maximum integer value.

%%cpp
auto s="1X2X3x";  // input undecimal string

auto value=0uLL;
constexpr auto m1=-1uL, m2=m1/11, m3=m1-10;
for (size_t i=0, length=strlen(s); i < length; ++i) {
    value *= value<=m2 ? 11 : throw runtime_error("The value is too big.");
    switch (unsigned char c=s[i]) {
    case 'X':
        [[fallthrough]];
    case 'x':
        value<=m3 ? value+=10 : throw runtime_error("The value is too big.");
        break;
    default:
        if ((c=s[i]-'0')>9) throw runtime_error("Invalid character found.");
        value<=m1-c? value+=c : throw runtime_error("The value is too big.");
    }
}
cout << format("Decimal value of the undecimal {} is {}.\n", s, value);
Decimal value of the undecimal 1X2X3x is 311376.

Distinct Prime Factors

Finally, consider the problem of computing the prime factors:

The following is an implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
for (auto p=2uLL; p<=n; p++) {     // check candidate factors from 2 to n
    if (n%p) continue;          // continue to the next factor
    bool isprime=true;          // a flag to keep track of the primality
    for (auto d=2uLL; d*d<=p; d++)  // check candidate divisors
        if (p%d==0) {           // divisor found for p
            isprime=false;
            break;              // break to skip the test of other divisors
        }
    if (isprime) {              // prime factor found
        cout << p << '\n';
        do n/=p; while (n%p==0);
    }
}

Program 6:Distinct prime factors of an unsigned integer n

Observe that the program prints the distinct prime factors of a positive integer n line by line in ascending order, regardless of how many prime factors there are:

%%cpp
auto n = 100uLL;  // input
cout << format("Distinct prime factors of {} in ascending order:\n", n);
for (auto p=2uLL; p<=n; p++) {
    if (n%p) continue;
    bool isprime=true;
    for (auto d=2uLL; d*d<=p; d++)
        if (p%d==0) {
            isprime=false;
            break;
        }
    if (isprime) {
        cout << p << '\n';
        do n/=p; while (n%p==0);
    }
} 
Distinct prime factors of 100 in ascending order:
2
5

Imperative programming focuses on using different control flow statements to make decisions and repeat actions in response to different conditions. In the following sections, we will learn how to write conditions and control flow statements.

Condition

Boolean Value

Control flow statements determine the flow of execution based on the program state, often summarized as a condition that can be regarded as true or false. Such a condition is said to take a boolean value, named after George Boole.

%%cpp
true
(bool) true
%%cpp
false
(bool) false

bool is a categorical type consisting of only the two values true and false. For instance, Program 6 declares a boolean variable isprime, which is initialized to true, may be updated to false later, and is used as a condition to decide whether to print the factor.

...
    bool isprime=true;          // a flag to keep track of the primality
    ...
            isprime=false;      // i is not prime
            ...
    if (isprime) ...            // prime factor found
...

Theoretically, a boolean value only requires 1 bit of storage, but C++ stores a boolean value with 1 byte:

%%cpp
sizeof(bool)
(unsigned long) 1
%%hermes
Why C++ uses one byte instead of one bit to store a boolean value? Can I use
just one bit to store a boolean value instead?
Loading...
🌿 Fork/new: main (will create on click)

Similar to Python, C++ regards true as 1, and false as 0:

%%cpp
true == 1 && false == 0
(bool) true
True == 1 and False == 0
True

We can even apply arithmetic operations on boolean values:

%%cpp
true + true > true && false * true == false
(bool) true
True + True, False * True
(2, 0)

Boolean Expression

A simple way to turn the program state to a boolean value is to apply comparison operators such as

<, <=, >, >=, <=>, ==, !=, <=>

to numeric values to form a boolean expression, and then combine boolean expressions into a compound boolean expression using logical operators such as

  1. negation: !

  2. logical AND: &&

  3. logical OR: ||.

Unlike C++, Python uses more expressive names for the logical operators: not vs !, and vs &&, or vs ||. In particular, 1 != 0 can be rewritten as !(1==0) in C++ but not Python.

Similar to Python, C++ regards any non-zero numeric value as true in boolean expressions. For instance, the following checks if all the variables are zero:

%%cpp
unsigned long x, y, z;
!(x + y + z)
(bool) true

Even though x + y + z is not boolean, C++ contextually converts it to boolean since the logical operators expect a boolean context for their operands. Contextual conversion is one of the implicit conversions in C++ that follows the following rule.

The value zero (for integral, floating-point, and unscoped enumeration) and the null pointer and the null pointer-to-member values become false. All other values become true.

%%hermes
Explain very briefly what is a null pointer-to-member value in C++?
Loading...
🌿 Fork/new: main (will create on click)
Solution to Exercise 1

Note that x + y + z without the logical operator ! is not contextually converted to boolean. For the code in the hints, x + y + z has a value of 2, which is not equal to true since true has an integer value of 1 instead.

Unlike Python, an empty C string "" is regarded as true but the null character '\0' is regarded as false:

%%cpp
auto s="";
auto c=s[0];
s && !c
(bool) true
not ("" and not "\0")
True
%%cpp
unsigned long x, y, z;
!(x || y || z)
(bool) true
Solution to Exercise 2

This is because the empty string "" is actually a pointer to a character array, not a null pointer. The null character \0 is regarded as integer 0, and therefore contextually converted to false.

A better check for all-zero is:

%%cpp
unsigned long x, y, z;
!(x || y || z)
(bool) true

An important benefit of using logical operator || in !(x || y || z) vs the addition + in !(x + y + z) is the short-circuit evaluation of logical expressions. To show this, run the following slightly modified code:

%%cpp
unsigned long x, y, z;
!(++x || ++y || ++z)
(bool) false
%%cpp
cout << format("x={}, y={}, z={}\n", x, y, z);
x=1, y=0, z=0

y and z are not incremented because ++x already evaluates to true, and so the execution of ++y and ++z are skipped as their values do not change the overall value of the boolean expression. In comparison, there is no short-circuiting for the addition operations:

%%cpp
int x, y, z;
!(++x + ++y + ++z)
(bool) false
%%cpp
cout << format("x={}, y={}, z={}\n", x, y, z);
x=1, y=1, z=1

Python also has short-circuit evaluation, but it returns the value of an operand in its original type without contextual conversion to boolean:

a = b = 0
a and b%a
0
a = 3
b = 5
a and b%a
2

In contrast, C++'s logical operations return a boolean as the operands are contextually converted to boolean:

%%cpp
int a, b;
a && b%a
(bool) false
%%cpp
int a=3, b=5;
a && b%a
(bool) true

Categories

It is possible to express a condition that takes multiple values. For instance, while most of the comparison operations evaluate to a boolean value, there is an exception—the three-way comparison operator <=>, also known as the spaceship operator, which returns a comparison category:

%%cpp
1 <=> 1
(std::strong_ordering) @0x633a57d44040

std::strong_ordering from <compare> is another categorical type similar to boolean, but with three possible categories below.[1]

  • std::strong_ordering::equal: The operands are equal.

  • std::strong_ordering::less: The first operand is less than the second.

  • std::strong_ordering::greater: The first operand is greater than the second.

%%cpp
auto order = (1 <=> 1);
order == strong_ordering::equal
(bool) true

The check for equality can also be simplified to:

%%cpp
order == 0
(bool) true

The check for the other two possible categories are as follows:

%%cpp
auto order = (1 <=> 2);
order == strong_ordering::less && order < 0
(bool) true
%%cpp
auto order = (2 <=> 1);
order == strong_ordering::greater && order > 0
(bool) true
%%hermes
What is the motivation for <=> in C++, given that one can use other comparison
operators to the same effect?
Loading...
↻ Session: main (ACP: 6b33a569-275…)

We can define custom categorical type using the enum declaration. E.g., the following declares the values for three-valued logic (TVL):

%%cpp
enum TVL {
    UNKNOWN=-1,
    FALSE=0,
    TRUE=1
};  // Is the semi-colon necessary?
TVL a=TRUE, b=FALSE, c=UNKNOWN;
cout << a << ' ' << b << ' ' << c << '\n';
a==true && b==false
1 0 -1
(bool) true

Manual assignments to integer values are optional, but they ensure that the integer values of TRUE and FALSE aligns with those of the corresponding boolean values true and false. Indeed, even the name of the enumeration is optional, as illustrated by the following unnamed enumeration:

%%cpp
enum {     // unnamed
    RED,   // default value 0
    BLUE,  // default value 1
    GREEN  // default value 2
};
auto a=RED, b=BLUE, c=GREEN;
cout << a << ' ' << b << ' ' << c << '\n';
a
0 1 2
(__cling_N534::(unnamed)) (__cling_N534::RED) : (unsigned int) 0

Unlike std::strong_ordering, the above enumerations are unscoped, which can lead to name conflicts if multiple such enumerations contain a value with the same name.

%%hermes
Give a concrete example of how unscoped enumerations in C++ can cause name 
conflicts.
Loading...
↻ Session: main (ACP: 6b33a569-275…)

To resolve the issue, C++11 introduced scoped enumerations, declared with enum class (and enum struct), such as:

%%cpp
enum class TVL {
    UNKNOWN=-1, U=UNKNOWN,        // short aliases
    FALSE=0, F=FALSE,
    TRUE=1, T=TRUE
};
TVL a=TVL::T, b=TVL::F, c=TVL::U;  // qualification `TVL::` required
a
(TVL) (TVL::TRUE) ? (TVL::T) : (int) 1

The above also added short aliases U, F, and T without worry of name conflicts because the qualification TVL:: is required to access the enumerators.

However, the following code will fail because scoped enumerations are not implicitly converted to integers, which is intended to ensure type safety:

cout << a << ' ' << b << ' ' << c << '\n';
a==true && b==false
%%cpp
static_cast<int>(TVL::TRUE)
(int) 1
%%hermes
Explain briefly how to properly define three-valued logic data type in C++ so 
that it works in boolean expressions?
Loading...
↻ Session: main (ACP: 6b33a569-275…)

Conditional

if statement

Now that we know how to specify a condition, we can begin to write programs with control flow statements.

The if statements is a control flow statement that executes a block of code based on whether a condition is true. For instance, in Program 1, there are two nested if statements:

if (a) {
    ... // executed when `a` is true
} else if (b<0) ... // executed when `a` is false but `b<0` is true

The first if statement executes a compound statement if the condition is true. Otherwise, the control goes to its else clause, which consists of a simple if statement without an else clause.

Unlike Python, the condition for the if statement must be enclosed by parentheses ( ... ), and there is no elif keyword to compress else if:

a = b = 0
if a:
    ...
elif b<0: ...
Solution to Exercise 3

The first if statement prevents a division by zero error when executing b % a if a equals 0. The second if statement converts b to its absolute value, which gives the desired GCD as a positive number even when a or b are negative.

Program 3 also has an if statement:

...
if (auto x_=1/y/y, 
    gap=(x>x_)?(x-x_):(x_-x), 
    abs_x=(x>0?x:-x), 
    abs_x_=(x_>0?x_:-x_); 
    y<0 || x_!=x && gap > 1e-9*(abs_x>abs_x_?abs_x:abs_x_)) { // inaccurate?
    ... //
}  // 

Different from the previous if statements, there is an initialization statement in front of the condition:

...
if (auto x_=1/y/y, 
    gap=(x>x_)?(x-x_):(x_-x), 
    abs_x=(x>0?x:-x), 
    abs_x_=(x_>0?x_:-x_);
    ...

The initializer defines variables local to the scope of the if statement. Hence, repeating the execution does not re-declare the variables in the global scope.

What does the condition mean, however?

    ...
    y<0 || x_!=x && gap > 1e-9*(abs_x>abs_x_?abs_x:abs_x_)) { // inaccurate?
...

Due to the limited precision of floating point numbers, the accuracy, or the closeness of two floating point numbers, say xx and x′x', should be determined with some tolerance.

In python, (1) is implemented by the function isclose from the math module:

from math import isclose
help(isclose)
Help on built-in function isclose in module math:

isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0)
    Determine whether two floating-point numbers are close in value.

      rel_tol
        maximum difference for being considered "close", relative to the
        magnitude of the input values
      abs_tol
        maximum difference for being considered "close", regardless of the
        magnitude of the input values

    Return True if a is close in value to b, and False otherwise.

    For the values to be considered close, the difference between them
    must be smaller than at least one of the tolerances.

    -inf, inf and NaN behave similarly to the IEEE 754 Standard.  That
    is, NaN is not close to anything, even itself.  inf and -inf are
    only close to themselves.

isclose(100, 90, rel_tol=0.1)  # because (100-90) == 0.1*100
True
isclose(100, 89, rel_tol=0.1)  # because (100-89) > 0.1*100
False
isclose(100, 90, abs_tol=10)    # because (100-90) == 10
True
isclose(100, 89, abs_tol=10)    # because (100-89) > 10
False

-inf, inf and NaN behave similarly to the IEEE 754 Standard:

inf = float('inf')
nan = float('NaN')
isclose(inf, inf) and isclose(-inf, -inf) and not isclose(nan, nan) 
True

Although there is no isclose function readily available in C++, it is not difficult to implement it. Program 3 implements it with the help of the conditional/ternary operator:

x>0 ? x : -x

The above is analogous to the conditional expression x if x>0 else -x in Python, which evaluates to the absolute value of x.

Solution to Exercise 4

This is because y is the inverse square root of x iff y is positive and the 1/y/y is close to x. More precisely, the initialization x_=1/y/y recovers x from the computed inverse square root to check if x is not equal to x_ and not close to x with a relative tolerance δrel=10−9\delta_{\text{rel}} = 10^{-9} and an absolute tolerance δabs=0\delta_{\text{abs}} = 0. The reason include x_!=x is to handle the special values inf and NaN according to the standard.

Unlike the conditional expression in Python, the conditional operator in C++ has to decide on the resulting data type based on the types of the operands, since C++ is statically typed.

%%cpp
true? 'a': 1L                           // why returns a `long`
(long) 97
%%cpp
false? 'a': false                       // why returns an `int`
(int) 0

As another example, the conditional expression in Program 5

value *= value<=m2 ? 11 : throw runtime_error("The value is too big.");

increases value by a factor of 11 only if there is no overflow. Otherwise, the throw expression will raise a runtime error and transfers the control to an exception handler, and if not handled, exits the program with an error message saying that “The value is too big.”[2] The assignment works because the conditional expression has type int even though the type of the throw expression is void:

%%cpp
true? 11 : throw
(int) 11

In contrast, the following code fails because of incompatible operand types.

value<=m2 ? 11 : cerr << "The value is too big.";

Unlike throw, the expression above is not void, but rather, the same type as that of cerr (to allow the operator << to be chained.) This case is not handled by the conditional operator, resulting in an error.

%%hermes
Explain very briefly how the resulting type of a conditional operator in C++ is
determined. Give concrete examples.
Loading...
↻ Session: main (ACP: 6b33a569-275…)

switch statement

Other than the if statement, C++ provides another conditional statement called the switch statement, which acts like the match statement in Python to switch execution among possibly more than two cases.

For instance, Program 5 uses the switch statement to convert a digit in an undecimal string to an integer value:

...
    switch (unsigned char c=s[i]) {   // condition in the form of a character declaration
    case 'X':                         // case label X
        [[fallthrough]];              // attribute to silent warning
    case 'x':
        ...
        break;                        // terminate the switch statement
    default:                          // default label
        ...
    }
...

Similar to the if statement, the condition in a switch statement is

  • enclosed in parentheses and,

  • optionally, follows an initialization of any number of variables of the same type.

However, unlike the if statement, where the condition is expected to be a boolean expression, the switch statement allows the condition to be of

  • an integral type (such as int, char, short, long),

  • an enumeration type (such as enum, enum class, enum struct), or

  • a class type that can be implicitly converted to an integral or enumeration type.

The condition can even be a variable declaration like (auto c=s[i]) above.

The body of the switch statement is typically a compound statement containing

  • several case labels, and

  • optionally a default label

When a switch statement is executed:

  1. The control is passed to the first case label that matches the value of the condition.

  2. If no such match exists, the control is passed to the default label. A break statement can be used to terminate the switch statement.

  3. Otherwise, the control will continue to flow to other cases after the matched case. Such a fall through may trigger a warning, but the attribute [[fallthrough]] introduced in C++17 can be used to silence the warning.

Solution to Exercise 5
  • The switch statement initializes the variable c as the character s[i] and uses it as the condition.

  • When c takes the value X or x, the control is passed to case 'X' or 'x', which eventually adds 10 to the decimal value.

  • [[fallthrough]]; silences the warning and allows the control to fall through to case 'x': to handle both uppercase and lowercase ‘X’.

  • For all other cases, the control is passed to the default label, which signals an error if c is not a valid digit; otherwise, it updates the decimal value with the value of the digit.

Another way to write the switch statement is as follows:

%%cpp
auto s="1X2X3x";  // input undecimal string

unsigned long long value;
unsigned char c;
constexpr auto m=-1uL;
for (unsigned long long i=0uLL, length=strlen(s); i < length; ++i) {
    switch (s[i]) {
    case '0': c = 0; break;
    case '1': c = 1; break;
    case '2': c = 2; break;
    case '3': c = 3; break;
    case '4': c = 4; break;
    case '5': c = 5; break;
    case '6': c = 6; break;
    case '7': c = 7; break;
    case '8': c = 8; break;
    case '9': c = 9; break;
    case 'X': [[fallthrough]];
    case 'x': c = 10; break;
    default: throw runtime_error("Invalid character found.");
    }
    value<=(m-c)/11 ? value=value*11+c : throw runtime_error("The value is too big.");
}
cout << format("Decimal value of the undecimal {} is {}.\n", s, value);
Decimal value of the undecimal 1X2X3x is 311376.
Solution to Exercise 6

The above program is less efficient than Program 5, which avoids checking the case labels '0' to '9' by directly converting the character '0' to '9' to an integer 0 to 9 using c-'0'.

%%cpp
auto s="1X2X3x";  // input undecimal string

auto value=0uLL;
constexpr auto m1=-1uLL, m2=m1/11, m3=m1-10;
for (size_t i=0, length=strlen(s); i < length; ++i) {
    value<=m2 ? value*=11 : throw runtime_error("The value is too big.");
    switch (unsigned char c=s[i]) {
    case 'X':
        [[fallthrough]];
    case 'x':
        value<=m3 ? value+=10 : throw runtime_error("The value is too big.");
        break;
    default:
        if ((c=s[i]-'0')>9) throw runtime_error("Invalid character found.");
        value<=m1-c? value+=c : throw runtime_error("The value is too big.");
    }
}
cout << format("Decimal value of the undecimal {} is {}.\n", s, value);
Decimal value of the undecimal 1X2X3x is 311376.

To show how the switch statement can be applied to enumerations, recall the TVL enumeration:

%%cpp
enum class TVL {
    UNKNOWN=-1, U=UNKNOWN,
    FALSE=0, F=FALSE,
    TRUE=1, T=TRUE
};

The following code randomly assigns a TVL value to a:

%%cpp
TVL a;
switch (rand() % 3-1) {
case 0: a=TVL::F; break;
case 1: a=TVL::T; break;
default: a=TVL::U;
}
a
(TVL) (TVL::TRUE) ? (TVL::T) : (int) 1
Solution to Exercise 7

No because enum class does not allow implicit conversion.

The following code implements the logical NOT operation on the randomly drawn value in a:

%%cpp
TVL b;
switch (a) {
case TVL::F: b=TVL::T; break;
case TVL::T: b=TVL::F; break;
default: b=a;
}
b
(TVL) (TVL::FALSE) ? (TVL::F) : (int) 0
Solution to Exercise 8

order cannot be used as a condition in switch because

  • it is neither an integer nor an enumeration, and

  • its type strong_ordering cannot be implicitly converted to an integer or an enumeration.

Iteration

while loop

Consider Program 2 for computing the GCD iteratively:

int c;
while (a) {  // execute repeatedly as long as a is non-zero
    ...
}
...

This is almost the same as its previous version Program 1 except that:

  1. The if keyword is changed to the while keyword so the body of the while loop gets executed repeatedly as long as the condition holds, i.e., a is non-zero.

  2. The declaration int c; is moved out of the local scope to avoid repeated declarations.

Consider Program 4 for computing the inverse square root iteratively:

...
double x_, gap, abs_x, abs_x_;
do {
    ...
    x_=1/y/y; gap=(x>x_)?(x-x_):(x_-x); abs_x=(x>0?x:-x); abs_x_=(x_>0?x_:-x_);
} while (y<0 || x_!=x && gap > 1e-9*(abs_x>abs_x_?abs_x:abs_x_));  // repeat if inaccurate

Instead of a while loop, the code uses a do-while loop, where the looping condition is checked after the execution of the body. This ensures that the accuracy is checked after the answer is updated.

When compared to the previous version Program 3:

  1. The declaration double x_, gap, abs_x, abs_x_; has moved out of the local scope because while/do-while loop does not support initializer in the condition, and even if it did support, the repeated declaration would be redundant.

  2. The update rule is modified to use the constant x2 and constant expression threehalfs to avoid repeated calculations and speed up the access to the values.

    constexpr auto threehalfs = 1.5, rel_tol = 1e-9;
    const auto x2 = x*0.5;
    auto x2 = x*0.5;
    double x_, gap, abs_x, abs_x_;
    ...
        y = y * (threehalfs - (x2 * y * y));
        ...
Solution to Exercise 9

There are no savings in computations because y2 must be re-computed every time y is updated. This is unlike threehalfs and x2, whose values are independent of y.

for loop

Instead of a while loop, Program 5 uses a for loop:

...
for (size_t i=0, length=strlen(s); i < length; ++i) {
    ...
}

This can be rewritten as a while loop:

...
{
    size_t i=0, length=strlen(s);
    while (i < length) {
        ...
        ++i;
    }
}

C++11 introduced a less error prone range-based for loop:

%%cpp
for (int i : {0, 1, 2, 3, 4}) cout << i << '\n';
0
1
2
3
4

The for loop declares i to take a value from the initializer list {0, 1, 2, 3, 4} one-by-one for each iteration. This is analogous to the Python code:

for i in 0, 1, 2, 3, 4:
    print(i)
0
1
2
3
4

C++23 also introduced <ranges> that makes the range of items more composable and lightweight:

%%cpp
for (int i : views::iota(0, 5))
    cout << i << '\n';
views::iota(0, 5)
0
1
2
3
4
(std::ranges::iota_view<int, int>) { 0, 1, 2, 3, 4 }

std::views::iota(0, 5) from <ranges> specifies the sequence of integers from 0 up to but excluding 5, like range in Python code:

for i in range(5):
    print(i)
range(5)
0
1
2
3
4
range(0, 5)

Finally, Program 6 uses a nested for loop to print the prime factors:

for (auto p=2; p<=n; p++) {     // check candidate factors from 2 to n
    ...
    for (auto d=2uLL; d*d<=p; d++)  // check candidate divisors
        ...
    ...
}

For each number p from 2 to n, it checks whether p is divisible by a number d from 2 up to the integer square root of p, using the condition d*d<=p.

In the outer loop, it uses the continue statement to continue to the next iteration:

for (auto p=2; p<=n; p++) {     // check candidate factors from 2 to n
    if (n%p) continue;          // continue to the next factor
    ... // code skipped by `continue`
}

This is equivalent to

for (auto p=2; p<=n; p++) {     // check candidate factors from 2 to n
    if (!n%p) {
        ... // code skipped by `continue`
    }
}
Solution to Exercise 10

When p does not divide n, the primality check can be skipped as p is certainly not a prime factor of n.

The code also uses the break statement to terminate the inner loop:

    ...
    for (auto d=2uLL; d*d<=p; d++)  // check candidate divisors
        if (p%d==0) {           // divisor found for `p`
            ...
            break;              // break to skip the test of other divisors
        }
    ... // `break` transfers the control to here

The code also uses a flag isprime to keep track of the primality of p to decide whether to print it as a prime factor later:

    ...
    bool isprime=true;          // a flag to keep track of the primality
    for (auto d=2uLL; d*d<=p; d++)  // check candidate divisors
        if (p%d==0) {           // divisor found for `p`
            isprime=false;
            break;              // break to skip the test of other divisors
        }
    if (isprime) cout << p << '\n'; // prime factor found
    ...
%%cpp
auto n = 524287uLL*2147483647uLL;  // input

cout << format("Distinct prime factors of {} in ascending order:\n", n);
for (auto p=2uLL; p<=n; p++) {
    if (!(n%p)) {
        cout << p << '\n';
        do n/=p; while (!(n%p));
    }
}
Distinct prime factors of 1125897758834689 in ascending order:
524287
2147483647
Solution to Exercise 11

The program is faster, without the need to check whether a factor p is prime in each iteration. To prove that the program is correct, it suffices to prove the loop invariant:

The base case holds trivially since n is not modified, and p==2 is the smallest possible prime factor. Assume as an inductive hypothesis that the loop invariant holds. Consider the non-trivial case where p divides n. p must be prime because, otherwise, p has a strictly smaller prime factor that divides the current n, contradicting the inductive hypothesis. In this case:

%%cpp
auto n = 524287uLL*2147483647uLL;  // input
cout << format("Distinct prime factors of {} in ascending order:\n", n);

for (auto p=2uLL; p*p<=n; p++) {
    if (!(n%p)) {
        cout << p << '\n';
        do n/=p; while (!(n%p));
    }
}
if (n>1) cout << n << '\n';
Distinct prime factors of 1125897758834689 in ascending order:
524287
2147483647
Solution to Exercise 12

The iteration only looks for the next prime factor up to ⌈n⌉\lceil \sqrt{n} \rceil for the current nn because, if there is a new factor bigger than that, there must also be one smaller than that. It prints nn at the end if n>1n>1 because it must be prime. Otherwise, the inner do-while loop would have reduced it. However, there is an performance issue when p*p<=n overflows, as explained in the solution of the next question.

%%cpp
auto n = 18446744073709551557uLL;  // input
cout << format("Distinct prime factors of {} in ascending order:\n", n);

for (auto p=2uLL; p<n/p+0.5; p++) {
    if (!(n%p)) {
        cout << p << '\n';
        do n/=p; while (!(n%p));
    }
}
if (n>1) cout << n << '\n';
Solution to Exercise 13

The termination condition is mathematically equivalent to

p * p <= n

and

p <= n / p

for positive integers p and n. Therefore, if arithmetic were exact, they would execute the same number of iterations and have the same asymptotic complexity.

The reason for preferring

p < n / p + 0.5

is not only that it avoid numerical precision issues but it also avoids overflow.

For an unsigned long long, the largest value is 264−12^{64}-1. The largest value whose square still fits is 232−12^{32}-1 because

(232−1)2=264−233+1<264−1.(2^{32}-1)^2=2^{64}-2^{33}+1<2^{64}-1.

Hence, when p≥232\geq 2^{32}, the expression p*p overflows and wraps modulo 264.

Interesting, overflow neither result in infinite loop nor produce an incorrect answer. Suppose the loop reaches p≥232\geq 2^{32}, then the remaining value of n cannot be composite, because any composite number less than 264 must have a prime factor at most n<232\sqrt{n}<2^{32}. Since no such factor was found, the remaining n must be prime. The loop will therefore continue until p==n, at which point n is divided out and becomes 1, causing the loop to terminate.

However, the running time becomes much worse, because p has to increase to n, i.e., O(n)O(n), instead of stopping before the square root of n, i.e., O(n)O(\sqrt{n}).

Therefore,

p <= n / p

does not improve the theoretical complexity when arithmetic is exact, but it prevents overflow and preserves the intended O(n)O(\sqrt{n}) running time. An alternative fix is p*p<=max(n, 1ULL<<32-1) but this makes the code dependent on the underlying integer type.

%%hermes
Explain briefly how to use techniques such as wheel factorization to further 
improve the computation of prime factors?
Loading...
↻ Session: main (ACP: 6b33a569-275…)
Footnotes
  1. The strong ordering in C++ implements the strict total ordering in order theory. There are two other more general category types:

    • std::weak_ordering: Non-strict ordering, which allows distinct items to be equivalent, e.g., in modular arithmetics.

    • std::partial_ordering: Partial ordering, which allows distinct items to be incomparable, e.g., in comparing sets.

  2. throw in C++ is similar to raise in Python except that raise is used in a statement instead of an expression.