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 6if (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 answer12
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 8int 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 answergcd(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 guessrsqrt(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 9y = (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 7constexpr 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 answerrsqrt(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 15auto 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 13for (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?🌿 Fork/new: main (will create on click)
Hermes
A small correction first: the C++ standard does not merely choose 1 byte for bool — it pins sizeof(bool) to exactly 1 ([basic.types]): a bool is a distinct, addressable type with its own objects, and the only size the standard allows is 1 byte. Two forces drive that choice:
The smallest addressable unit of memory is a byte. The C++ abstract machine (and every common ISA) reads and writes whole bytes; there is no hardware instruction that can load or store a single bit. Since
boolmust be assignable and take the address of its object, it must own an addressable unit — 1 byte is the minimum. Makingboolbigger would waste 7 of every 8 bits for no expressiveness.It stays portable and simple. A 1-byte guarantee means
boolworks identically everywhere, and&bon abool bgives a realbool*pointing at a real object — no special cases in the type system.
Can you use 1 bit instead? Yes — by packing bits, which is exactly what sizeof_bool.ipynb demonstrates. Three standard ways:
std::bitset<N>—Nbits fixed at compile time;flags[0]/flags[7]each own one bit (printed as10000001).std::vector<bool>— a dynamic container of one bit per element: 1,000,000 booleans in 125,000 bytes (1/8 the size ofvector<char>).Manual packing — several booleans living in one byte (
01010101= four booleans sharing a byte).
The price of bit-packing is the reason a plain bool keeps a whole byte:
a bit has no address of its own — there is no
bool*pointing at a single bit, and invector<bool>the element is a proxy type, not a realbool&;every access is a read-modify-write of the whole word holding the bit, not a single fast store.
So: for single flags, use bool (1 byte, simple, addressable); when booleans arrive in bulk and density matters — flags for a million items, bitmasks, Bloom filters, compressed adjacency — use bit-packed storage and pay the read-modify-write cost deliberately.
Similar to Python, C++ regards true as 1, and false as 0:
%%cpp
true == 1 && false == 0(bool) true
True == 1 and False == 0TrueWe 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
negation:
!logical AND:
&&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++?🌿 Fork/new: main (will create on click)
Hermes
Null pointer-to-member value (C++ standard [conv.ptr]):
A pointer-to-member (T C::*) holds one special value — the one you get by converting nullptr to that type. That’s the null pointer-to-member value, and it’s the only value of a pointer-to-member that converts to false in a boolean context; every other value converts to true.
The subtlety: unlike a pointer-to-object, its bit pattern is not guaranteed to be all zeros, because a pointer-to-member must encode both the member’s offset and which class it belongs to — so even the “null” one can be a non-zero representation (e.g. carrying the class’s type info). It’s defined by the nullptr conversion, not by its bits.
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%a0a = 3
b = 5
a and b%a2In 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?↻ Session: main (ACP: 6b33a569-275…)
Hermes
Two motivations:
1. One expression yields the full relationship as data. The other operators each answer only one yes/no question. To recover the three-way result from a<b / a==b you’d need a comparison chain (a<b ? less : a>b ? greater : equal), comparing the operands twice. <=> compares once and hands you the whole category, which is what algorithms like std::sort actually need — and, as the cells above show, you can then test the category (order == 0, order < 0, or against strong_ordering::equal).
2. Operator synthesis (the main reason it exists for user types). Pre-C++20, making a class fully comparable meant defining six operators — <, <=, >, >=, ==, != — and keeping them consistent by hand. The classic bug is < and == disagreeing (e.g. ordering by one field, equality by another). With <=> you implement just one (operator<=>, optionally plus operator==) and the compiler synthesizes the other five, so consistency is guaranteed by construction. The standard library (std::ranges::sort, std::min/max, std::compare) is designed around this, so a single definition plugs into all of it.
So: for built-in types it’s a convenience + a single comparison; for your own types it’s correctness — one definition, no six-operator drift.
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==false1 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';
a0 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.↻ Session: main (ACP: 6b33a569-275…)
Hermes
The problem is that unscoped enums leak their enumerators into the surrounding scope (here, global scope), so a later unscoped enum can’t reuse any of those names.
Concrete example — extend the notebook with a second unscoped enum for a traffic light (which naturally also has red/green and an unknown state):
enum TVL { UNKNOWN=-1, FALSE=0, TRUE=1 }; // from cell [93]
enum { RED, BLUE, GREEN }; // from cell [96]
enum TrafficLight { UNKNOWN, RED, GREEN }; // ❌ compile errorResult:
error: 'UNKNOWN' conflicts with a previous declaration → previous declaration 'TVL::UNKNOWN'
error: 'RED' conflicts with a previous declaration
error: 'GREEN' conflicts with a previous declarationWhy: RED, GREEN, UNKNOWN were already declared in the global namespace by the earlier enums, so TrafficLight’s enumerators collide with them — the names don’t belong “inside” their enum, they belong to the scope that contains the enum.
The fix is to scope the names (enum class), which is exactly the contrast the notebook draws with std::strong_ordering::equal:
enum class TrafficLight { UNKNOWN, RED, GREEN }; // ✅ compiles
// use as TrafficLight::RED, not bare REDwith scoped enums the enumerators live inside the type’s scope, so TrafficLight::RED and the old RED coexist happily.
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?↻ Session: main (ACP: 6b33a569-275…)
Hermes
The notebook has set this up: enum class TVL { UNKNOWN=-1, FALSE=0, TRUE=1 } avoids the name conflicts, but it shows the price — a scoped enum doesn’t convert implicitly to int/bool, so if (v) fails to compile. To make TVL usable in boolean expressions, wrap it in a type with an explicit operator bool:
enum class TVL { UNKNOWN=-1, FALSE=0, TRUE=1 };
struct TVLx {
TVL v;
constexpr explicit operator bool() const noexcept { return v == TVL::TRUE; }
};This works everywhere C++ does contextual conversion — if/while/for conditions, !, &&, ||, the condition of ?:, and static_cast<bool>:
TVLx t{TVL::TRUE}, u{TVL::UNKNOWN};
if (t) {} // ✓
bool a = !u; // ✓ (UNKNOWN → false → a = true)
bool b = (t && u); // ✓ (false)
bool c = (t || u); // ✓ (true)
bool e = static_cast<bool>(u); // ✓ (false)Two things that make it proper rather than sloppy:
explicitis required, and it’s a feature. A freeoperator bool(TVLx)is ill-formed (conversion operators must be non-static members), and a non-explicit one would leak implicit bools everywhere —bool b = x;,int i = x;,cout << x— silently collapsing three values into two. Withexplicit, the conversion only happens at the contextual-conversion sites above;bool b = x;elsewhere is a compile error (all verified).The truth table is a policy decision, not a free lunch. C++ itself has no three-valued logic — it only asks is this expression true? and expects a yes/no. So you must choose what
UNKNOWNmeans when pressed: hereUNKNOWNis falsy (v == TVL::TRUE). If you’d rather treatUNKNOWNas truthy (“definitely not false”), flip the test tov != TVL::FALSE. Whichever you pick, the third value gets flattened at each boolean boundary — soUNKNOWNis only truly preserved when it flows through comparisons (u == TVL::UNKNOWN) or non-boolean code.
If you don’t want a wrapper, the idiomatic alternative is to keep the plain enum and route checks through a named function, constexpr bool is_true(TVL v) { return v == TVL::TRUE; }, which keeps the policy visible at each use site.
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 trueThe 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 and , 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*100Trueisclose(100, 89, rel_tol=0.1) # because (100-89) > 0.1*100Falseisclose(100, 90, abs_tol=10) # because (100-90) == 10Trueisclose(100, 89, abs_tol=10) # because (100-89) > 10False-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) TrueAlthough 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 : -xThe 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 and an absolute tolerance . 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.↻ Session: main (ACP: 6b33a569-275…)
Hermes
C++ decides the result type statically — it looks at the types of the two arms (never at which arm is actually taken) and finds the single type both must convert to. (Verified with static_assert on each case below.)
The rules ([expr.cond]):
One arm is
throw: the other arm’s type wins —true ? 11 : throwisint.Both arms are class types: a common base class —
true ? Base{} : Derived{}isBase(the result is an lvalue of the base).Otherwise: both arms are converted to their standard types, and the result is the common type via usual arithmetic conversions:
true ? 'a' : 1L→long(charwidens tolong)false ? 'a' : false→int(charandboolboth promote toint)true ? 11 : 11.0→doubleint x = 1, y = 2; x < y ? x : y→int&(special case: if both arms are same-type lvalues, the result is an lvalue of that type — so you can assign to it)
And if no common type exists, it’s a compile error — that’s why value<=m2 ? 11 : "x" fails: int and const char* can’t be unified.
The contrast with Python is the point of the section: Python’s a if c else b returns whichever object was chosen (no common type needed); C++ must pick one type up front, so mismatched arms are ill-formed rather than a runtime surprise.
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), ora 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
caselabels, andoptionally a
defaultlabel
When a switch statement is executed:
The control is passed to the first
caselabel that matches the value of the condition.If no such match exists, the control is passed to the
defaultlabel. Abreakstatement can be used to terminate theswitchstatement.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.
Explain how the switch statement in Program 5 converts a digit in the undecimal string to its decimal value.
Solution to Exercise 5
The
switchstatement initializes the variablecas the characters[i]and uses it as the condition.When
ctakes the valueXorx, the control is passed to case'X'or'x', which eventually adds10to the decimal value.[[fallthrough]];silences the warning and allows the control to fall through tocase '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
cis 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.
Is the above program more efficient than Program 5 in converting an undecimal string of all 9’s such as
“999999999999999999”
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
Explain whether we can simplify the above code to
TVL a = rand()%3 - 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
The following code tries to compare two randomly drawn integers a and b:
int a=rand(), b=rand();
switch (auto order=a<=>b; order) {
case strong_ordering::equal:
cout << "a == b\n";
break;
case strong_ordering::less:
cout << "a < b\n";
break;
default:
cout << "a > b\n";
}What is wrong with the code?
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_orderingcannot 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:
The
ifkeyword is changed to thewhilekeyword so the body of thewhileloop gets executed repeatedly as long as the condition holds, i.e.,ais non-zero.The declaration
int c;is moved out of the local scope to avoid repeated declarations.
A simple line of redundant code in an iteration can be costly since it may run many times.
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 inaccurateInstead 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:
The declaration
double x_, gap, abs_x, abs_x_;has moved out of the local scope becausewhile/do-whileloop does not support initializer in the condition, and even if it did support, the repeated declaration would be redundant.The update rule is modified to use the constant
x2and constant expressionthreehalfsto 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)); ...
Why not define auto y2 = y * y to save some more computations in the update rule?
y = y * (threehalfs - (x2 * y2))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;
}
}for loop is often preferred as it is easier to read and safer to write. E.g., in the above example, it is easier to forget to increment i in the while loop, resulting in an infinite loop.
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`
}
}In Program 6, why is it okay to continue to the next outer loop under the condition n%p?
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 hereA break statement can only terminate the closest enclosing loop or a switch statement, but not the if statement, i.e., the following code will fail:
if (true) break;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
...How does the following code improve Program 6 in computing the prime factors?
The code takes about 15 seconds to factor n, which is a product of two large Mersenne primes.
%%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:
At the beginning of the for loop:
The set of prime factors smaller than p are already printed out.
The set of prime factors of the current n are those of the original input n at least p.
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:
The body of the
ifstatement is executed to print the prime factorp, fulfilling Condition 1 of the loop invariant at the beginning of the nextforloop.The inner
do-whileloop removes the prime factorpfromn, fulfilling Condition 2 of the loop invariant] at the beginning of the nextforloop.
How does the following code further improves the computation of prime factors?
%%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 for the current because, if there is a new factor bigger than that, there must also be one smaller than that. It prints at the end if 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.
How does the following code further improves the computation of prime factors?
%%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 <= nand
p <= n / pfor 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.5is not only that it avoid numerical precision issues but it also avoids overflow.
For an unsigned long long, the largest value is . The largest value whose square still fits is
because
Hence, when p, 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, then the remaining value of n cannot be composite, because any composite number less than 264 must have a prime factor at most . 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., , instead of stopping before the square root of n, i.e., .
Therefore,
p <= n / pdoes not improve the theoretical complexity when arithmetic is exact, but it prevents overflow and preserves the intended 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?↻ Session: main (ACP: 6b33a569-275…)
Hermes
p*p<=n already bounds the search, but still tries every integer 2,3,4,5,.... The key observation: once you’ve divided out all 2s and 3s, no remaining factor of n is divisible by 2 or 3 — so every multiple of 2 or 3 is a guaranteed dead-end trial. Wheel factorization skips them by construction.
The idea. Pick a modulus W built from small primes and keep only residue classes that a prime > W’s smallest factor could occupy. For W = 30 = 2·3·5, a prime p > 5 must be coprime to 30, so p ≡ 1, 7, 11, 13, 17, 19, 23, 29 (mod 30). Test 2, 3, 5 explicitly, then jump only between these classes. Since only 8 of 30 classes are candidates, that’s 3.75× fewer mod operations. For the simpler 2·3 wheel, primes >3 are ≡ 1, 5 (mod 6), giving steps +2, +4, +2, +4, ... and a 3× reduction.
Concretely — the candidate sequence is 5, 7, 11, 13, 17, 19, 23, 25, 29, 31, ..., and the gaps between candidates form a repeating pattern, so the loop just cycles a small table:
// 2, 3, 5 handled first; then walk the 30-wheel
static const u64 gaps[] = {6,4,2,4,2,4,6,2}; // 29->31(+2), 31->37(+6), ...
for (u64 p = 5, i = 0; (u128)p*p <= n; ) {
if (!(n%p)) { cout << p << '\n'; do n/=p; while (!(n%p)); }
p += gaps[i++ % 8];
}
if (n>1) cout << n << '\n';Two things to keep in mind:
Nothing is missed. Any prime factor
p > 5of the reducednis coprime to 30, hence in the candidate set, and thep*p <= nbound still stops the walk in time — theif (n>1)tail catches a leftover large prime, exactly as in cell [194].Candidates aren’t all prime.
25,35,49also fit a wheel class and will be tried — but only numbers that could be prime (no multiples of 2, 3, or 5) reach the test, and the innocent composites are rejected byn%p != 0. Bigger wheels (2·3·5·7 = 210, φ(210)/210 = 48/210 ≈ 4.4× fewer trials; 2·3·5·7·11 = 2310, ≈ 5.8×) just extend the gap table.
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.
throwin C++ is similar toraisein Python except thatraiseis used in a statement instead of an expression.