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.

Abstract

Motivated by practical problems such as computing the greatest common divisor (GCD) and the inverse square root, this notebook introduces the fundamental elements of computation in C++. It explores C++ syntax for declaring variables and representing values of various types, including integers, floating-point numbers, characters, strings, and pointers. The design of C++ is compared with that of Python to highlight key differences in type systems, memory management, and operator behavior across programming languages.

from __init__ import *

Motivations

This notebook introduces the basic ingredients for computations in C++ and explores their limitations. To provide motivation for the subject, we begin by presenting two problems below.

GCD

Consider the problem of computing the greatest common divisor (GCD), which is a fundamental concept in number theory.[1]

The following is an implementation in C++.

  1. To specify and store the non-zero integers aa and bb:

%%cpp
int a=2*3*4, b=3*4*5; // integer variables declaration and initialization
cout << format("gcd({}, {})=?\n", a, b); // print formatted string
  1. To compute the GCD of aa and bb step-by-step, repeatedly run the following program until a becomes 0, in which case the absolute value of b is the gcd.

1
2
3
4
5
6
{
    b = b % a; // assignment and modulo operation
    int c = b; // variable in the block scope of the compound statement
    b = a;
    a = c;
} // compound statement

Program 1:Computation of the GCD of the non-zero integers a and b.

%%cpp
{
    b = b % a;
    int c = b;
    b = a;
    a = c;
    cout << format("gcd({}, {})\n", a, b); // intermediate answer
} // compound statement
Solution to Exercise 1

a or b cannot be too large, exceeding the range of int, e.g., with a=2147483648 (231) if int is 32-bit.

If you can answer the above questions, great! But don’t worry if you cannot. We will learn some basic programming elements to solve Exercise 1 in this notebook.

Solution to Exercise 2

The algorithm requires n−1n-1 iterations for a=Fn−1a=F_{n-1} and b=Fnb=F_{n} for n>2n>2 because

Fn−2=Fnmod  Fn−1.F_{n-2} = F_n \mod F_{n-1}.

In terms of computational complexity, Euclidean algorithm is quite efficient O(log⁡min⁡{ ∣a∣,∣b∣ })O(\log \min\Set{\lvert a\rvert,\lvert b\rvert }). However, to handle large integers, optimizations such as Lehmer’s algorithm can be used.

Exercise 2 is more challenging, as it requires quite a bit of computational thinking that you will develop over time, mostly outside the classroom. Dive into it with the help of AI:

%%hermes
What is the fastest possible algorithm for computing GCD?

The following benmarks different GCD algorithms in Python:

from gcd import *

# Consecutive Fibonacci numbers
n = 100_000
f1 = fib(n)
f2 = fib(n - 1)

# warmup
gcd_euclid(f1, f2)
gcd_binary(f1, f2)
gcd(f1, f2)

t0 = perf_counter()
g1 = gcd_euclid(f1, f2)
t1 = perf_counter()

g2 = gcd_binary(f1, f2)
t2 = perf_counter()

g3 = gcd(f1, f2)
t3 = perf_counter()

print("Euclid  :", t1 - t0, "seconds")
print("Binary  :", t2 - t1, "seconds")
print("Library :", t3 - t2, "seconds")
print("GCDs equal:", g1 == g2 == g3)

Inverse Square Root

While computing the greatest common divisor (GCD) involves only integers, many computations involve rational, real numbers, and even complex numbers. An example is to compute the inverse/reciprocal square root

rsqrt⁡(x):=1xfor x>0,\begin{align} \operatorname{rsqrt}(x)&:=\frac{1}{\sqrt{x}} && \text{for $x>0$}, \end{align}

which is an important computation for 3D graphics, e.g., in first-person-shooting games such as Quake III Arena.

The following is an implementation in C++.

  1. Store the real number xx approximately as a floating-point number, and compute its reciprocal y=1xy=\frac1x as the initial guess of the inverse square root:

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

auto y = 1/x;      // reciprocal, type `auto`matically deduced
cout << format("Initial guess: {}\n", y);
  1. To compute the desired square root of the reciprocal, repeatedly runs the following until the answer remains unchanged/steady.

1
y = (y+1/y/x)/2;   // update the answer

Program 2:Update rule for the inverse square root of x.

%%cpp
y = (y+1/y/x)/2;
cout << format("Intermediate answer: {}\n", y);

You can verify whether the answer is correct by recovering xx from yy as 1y2\frac{1}{y^2}:

%%cpp
1/y/y

The program can be improved by:

  1. A better initial guess

1
2
3
 auto i = *reinterpret_cast<int64_t *>(&x);   // copy x to i as an integer
 i = 0x5fe6eb50c7b537a9 - (i >> 1);           // ???
 auto y = *reinterpret_cast<double *>(&i);    // copy i to y as a double

Program 3:Initial guess for the fast inverse square root of x.

%%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
  1. An alternative update rule

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

Program 4:Update rule for the fast inverse square root of x.

%%cpp
y = y * (1.5 - (x/2 * y * y));
cout << format("Intermediate answer: {}\n", y);
%%cpp
1/y/y
Solution to Exercise 1

x cannot be too large or to close to 0, e.g., with x=1e400 or x=0, due the limited precision of the floating point numbers.

Solution to Exercise 4

The update rules are from the Newton's method. In particular, the first one in Program 2 is obtained by thinking of the answer as a root of the function

f(y)=y2−1x,f(y)=y^2-\frac1{x},

while the second one Program 4 is obtained by thinking of the answer as a root of the function

f(y)=1y2−x.f(y)=\frac{1}{y^2}-x.

In Program 3 auto i = *reinterpret_cast<int64_t *>(&x);,

  1. &x obtains the address of x,

  2. reinterpret_cast<int64_t *> converts it to the pointer for a 64-bit integer type int64_t, and

  3. auto i = *... reads the binary representation of the floating-point number x as the binary representation of an integer, and store it as i. auto y = *reinterpret_cast<double *>(&i); reads the integer bits of i back to the floating-point bits and store it as y.

It remains to understand the following magical line

i = 0x5fe6eb50c7b537a9 - (i >> 1);   // ???

which exploits the floating point representation. For a detailed explanation, see the following video or Fast inverse square root#Floating-point representation. For the version using double instead of float, see A Brief History of InvSqrt.

Answering both Exercise 3 and Exercise 4 requires a very good understanding of how real numbers are represented in computers in addition to the numerical methods involved:

%%hermes
List different methods of computing the square root using basic arithmetic 
operations.

The following benmarks different inverse square root algorithms in Python:

from rsqrt import *

x = 10.0 / 3.0
print(f"x = {x}")

print(f"bisect : {rsqrt_bisect(x):.15f}")
print(f"newton : {rsqrt_newton(x):.15f}")
print(f"math   : {1/sqrt(x):.15f}")
print(
    f"digits : rsqrt(1056) ~= "
    f"{rsqrt_digits(1056):.15f}"
)

REPEAT = 100000

# warmup
rsqrt_bisect(x)
rsqrt_newton(x, verbose=False)
1.0 / sqrt(x)

t0 = perf_counter()
for _ in range(REPEAT):
    rsqrt_bisect(x)
t1 = perf_counter()

for _ in range(REPEAT):
    rsqrt_newton(x, verbose=False)
t2 = perf_counter()

for _ in range(REPEAT):
    1.0 / sqrt(x)
t3 = perf_counter()

print(f"Bisection : {(t1-t0)/REPEAT:.3e} s")
print(f"Newton    : {(t2-t1)/REPEAT:.3e} s")
print(f"Library   : {(t3-t2)/REPEAT:.3e} s")

In the sequel, we will tackle the easier task of learning the elements of C++ involved in carrying out the above computations.

Integer

Literal

How to specify an integer value in C++?

One way to specify a value is to use a literal, which is a fixed value directly embedded in the source code. A literal can be written in different ways:

%%cpp
15 // in decimal
%%cpp
0b1111 // in binary
%%cpp
017 // in octal
%%cpp
0xF // in hexadecimal

All of the above have the same value of type int!

In comparison, Python also uses the same syntax except for octal format:

15, 0b1111, 0o17, 0xF

C++ also supports different types of integer literals of different sizes, e.g., 3 billions is represented as type long:

%%cpp
3000000000

1019 is represented as unsigned integer type:

%%cpp
10000000000000000000 // with a warning

We can use the appropriate suffixes to specify the desired type:

%%cpp
10000000000000000000uLL // no warning message
%%cpp
1L // not the default type `int`.

The size of different types can be obtained by the sizeof operator[2]:

%%cpp
sizeof(short)  // in the unit of byte (8 bits)
%%cpp
sizeof(int)
%%cpp
sizeof(long)
%%cpp
sizeof(long long)

Let’s calculate the number of values of type int:

%%cpp
1uLL << sizeof(int)*8

We can also obtain the exact range of a number type using the std::numeric_limits class template from the header <limits>:

%%cpp
cout << format("Range of int: {{{} ... {}}}\n", 
               numeric_limits<int>::min(),  
               numeric_limits<int>::max());

The number of values of type int can alternatively be computed as follows:

%%cpp
1uLL + numeric_limits<int>::max() - numeric_limits<int>::min()

You might have noticed that the actual size of int is not the same as the required size specified in the C++ standard. Let’s ask AI about it:

%%hermes
Why int in C++ is 32 bits instead of 16 bits required by the C++ standard?

Similarly, the size of long can be 32 bits or 64 bits depending on the computer/compiler used. To ensure a definite range across different implementations, fixed integer types are available from <cstdint>.

%%cpp
1uL - numeric_limits<int16_t>::min() + numeric_limits<int16_t>::max() == 1uL << 16

For instance, Program 3 uses int64_t to ensure that the integer type has 64 bits, matching the size of the type of x to be explained in the section Floating Point Number.

Solution to Exercise 5

If 1 is used instead, the expression has a value 0 because the count overflows and wraps around to 0. (This is different from the previous calculation using the bitwise shift operation.) We used uLL instead of L because L or uL may not have large enough range as they can sometimes have 32 bits only, depending on the computer and compiler. unsigned type is used since the return value is expected to be non-negative.

Variable

Knowing how to specify an integer, we need a way to store it and retrieve it to perform more complicated computations.

The following declares integer variables with the default initialization of value 0 for variables with static storage duration such as global variables.

%%cpp
int a;
short b, c;
unsigned long d, e, f;
cout << format("{} {} {} {} {} {}\n", a, b, c, d, e, f);

int a; is an expression statement (ended with a semi-colon) that declares an integer variable named a.

A variable name must be a valid identifier. In particular, similar to Python, it should start with a letter or an underscore, and should not be one of the keywords. For instance, the following declarations fail:

int 1a;
int this;

Variables without static storage duration such as the following variables defined in a block scope are not initialized:

%%cpp
{
    int a;
    short b, c;
    unsigned long d, e, f;
    cout << format("{} {} {} {} {} {}\n", a, b, c, d, e, f);
}

To initialize the variables explicitly, the following uses the copy initialization:

%%cpp
auto a = 1;
short b = a+1, c = b+1;
auto d = 1uL;
cout << format("{} {} {} {}\n", a, b, c, d);

In the above code, auto is a placeholder type specifier that allows the compiler to automatically deduce the data type based on the initializer:

%%cpp
a
%%cpp
d

Note that a variable is available immediately after the locus/point of declaration. It is really immediately:[4]

%%cpp
int one=one+1; // `auto` would not work obviously.
one
%%hermes
Explain briefly whether `one` in the following code is initialized as `one+1`? 
Or `1`? or Or `0`?
---
int one=one+1;

The copy initialization involves an extra copy step, which can be costly for composite data consisting of many values. The extra copy step can be avoided using direct initialization:

%%cpp
int one(one+1);
one

Let’s play with the copy and direct initialization some more:

%%cpp
short b(100000);    // runs without error! Why?
b
%%cpp
unsigned short d=-1;  // runs without warning! Why?
d

To catch bugs caused by narrowing, C++11 introduced list initialization:

%%cpp
int one{one+1};
one

This initialization not only avoid an extra copy step associated with the copy initialization, it also prevents information loss due to narrowing. In particular, the following fails as narrowing is checked by list initialization:

%%cpptutor
int main() {
    short b{100000};
    unsigned long d{-1};
    return 0;
}

Operator

The values of variables can be modified using assignment operators:

%%cpp
unsigned long d, e, f;
d = e = f = 1;
cout << format("{} {} {}\n", d, e, f);

d = e = f = 1 behaves like chained assignment in Python:

d = e = f = 1
print(d, e, f)

Similar to the augmented assignment operators in Python, C++ also has compound assignment operators such as +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=:

%%cpp
f += e += d += 1;
cout << format("{} {} {}\n", d, e, f);

However, f += e += d += 1 is not a valid syntax in Python. How does assignment work in C++?

  1. In C++, assignment operators are right associative and so the evaluation is equivalent to

    f += (e += (d += 1))

    In comparison, assignment operators are non-associative in Python.

  1. In C++, an assignment operation has a value equal to the assigned value, so

    • (d += 1) evaluates to d+1;

    • (e += d+1) evaluates to e+d+1; and

    • (f += e+d+1) evaluates to f+e+d+1.

    In comparison, an assignment is usually a statement in Python that does not have a value.[5]

Note that assignment and initialization are different operations. For instance, a variable can be declared to be a constant using const:

%%cpp
const int one=one+1; // cannot write `const int one+=1;`
// one=one+1;        // can initialize but not assign a value to a constant
cout << one;

A constant is stored in read-only memory that cannot be modified after initialization. While the default initialization and copy initialization in const int one=one+1; are okay, the assignment operation one=one+1 is not. There is no compound initialization like const int one+=1; either.

Different programming languages may implement different operators, or have different implementations for the same operator.

For instance, while C++ has no exponentiation operator **, unlike Python, it has the suffix/prefix increment/decrement operators ++/--, which are not available in Python:

%%cpp
int x = 0;
int y = x++; // increments x after evaluation
int z = --x; // decrements x before evalution
x == y && y == z && z == 0  // all zero?

The last expression utilizes the comparison operator == (not =) to check equality and combine these checks using the logical AND operator &&. In general, comparison operators have higher precedence than logical operators and so they are evaluated first, i.e.,

(x == y) && (y == z) && (z == 0)

In Python, you can achieve the same effect using a chained comparison:

ROOT.x == ROOT.y == ROOT.z == 0
%%cpp
int x = y = z = 0;
x == y == z
Solution to Exercise 6

The chaining in C++ works differently than the chained comparison in Python.

  • x == y == z evaluates to true == z since == is left-associative and x == y evaluates to true; and

  • true == z evaluates to false since z is 0, not `true.

Program 1 uses the modulo operation a % b to give the remainder of a divided by b. However, the behavior can be confusing for negative operands, i.e., when a or b are negative. Program 1 can give different results if written in a different programming language or hardware because the modulo operator may have a different implementation.

For C++:

%%cpp
int r0=  5 %  3;
int r1= -5 %  3;
int r2=  5 % -3;
int r3= -5 % -3;
cout << format("{}, {}, {}, {}\n", r0, r1, r2, r3);

For Python:

print("{}, {}, {}, {}\n".format(5 % 3, -5 % 3, 5 % -3, -5 % -3))
Solution to Exercise 7

The sign of rr follows that of the dividend (divisor) for C++ (Python). E.g., for C++,

−5=3⋅(−1)⏞q+(−2)⏞r5=−3⋅(−1)+2\begin{alignat}{3} -5 &= &3 &\cdot \overbrace{(-1)}^{q} +&\overbrace{(- 2)}^{r}&\\ 5 &= &-3 &\cdot (-1) + &2& \end{alignat}

The quotient qq in (7) can be obtained by the division operation / operation.

%%cpp
int q0=  5 /  3;
int q1= -5 /  3;
int q2=  5 / -3;
int q3= -5 / -3;
cout << format("{}, {}, {}, {}\n", q0, q1, q2, q3);

In Python, // is used instead for integer division. Similar to %, the implementation for // is also slightly different from / in C++.

print("{}, {}, {}, {}\n".format(5 // 3, -5 // 3, 5 // -3, -5 // -3))
%%hermes
Explain why integer division is implemented differently in 
C++ versus Python.

Character

How to represent a character?

A character literal is a character delimited by single quotes.

%%cpp
'f'

Each value of type char is represented by 1 byte.

%%cpp
sizeof(char)

char is actually an integer type, e.g., we can initialize variables of type char with integer values as follows.

%%cpp
char a = 65, b(66), c {67};
cout << format("{} {} {}", a, b, c);

We can also perform arithmetic operations on characters like what we can do on integers:

%%cpp
a - b * 2

char can also be signed or unsigned:

%%cpp
numeric_limits<char>::min()           // sign bit (left-most bit) equals 1
%%cpp
numeric_limits<unsigned char>::min()  // 0

A character is represented by an integer according to the ASCII code. The following converts between int and char using static type casting:

%%cpp
cout << format("The ASCII code of {} is {}.\n", static_cast<char>(65), static_cast<int>('A'));
%%hermes
List and explain very briefly the different types of cast in C++. Give
a one-liner example for each cast.

Be careful that not every character can be printed. E.g., the last character in the ASCII code is DEL (delete), which cannot be printed.

%%cpp
cout << format("The ASCII code of {} is {}.\n", static_cast<char>(127), 127);
%%cpp
// cout << static_cast<char>(128);   // fails
static_cast<char>(128)               // works
Solution to Exercise 8

128 is outside the range (0-127) of valid ASCII code. ASCII code uses 7-bit, while char uses 8-bit. Hence, half of the possible values of char cannot be decoded to a character.

As another example, the first character in the ASCII code is NUL (null), which cannot be printed either.

%%cpp
cout << format("The ASCII code of {} is {}.\n", static_cast<char>(0), 0);

Note that it is missing a few more characters at the end, namely, is 0.. Why?

%%hermes
Why the following C++ code only prints "The ASCII code of"?
---
cout << format("The ASCII code of \{\} is \{\}.\n", static_cast<char>(0), 0);

Floating Point Number

Declaration

Computations on real numbers are essential for many applications such as simulations, modeling, computer graphics, and machine learning, etc. To manipulate real numbers, a simple idea is to approximate a real number by a rational number, which can then be represented by two integers, namely, the numerator and denominator. For instance,

π≈227.\pi\approx \frac{22}{7}.
%%cpp
22/7

That is not quite what we expected! While you may know how to fix the above issue, it highlights the need for a more convenient representation for numerical computations—floating-point arithmetics.[8]

%%cpp
22/7.  // the point is not the period
Solution to Exercise 9

The same operator / produces different results in 22 / 7 and 22 / 7. due to the types of the operands. In 22 / 7, both operands are integers, resulting in integer division, which discards the fractional part. In contrast, 22 / 7. involves a floating-point literal (7.), causing C++ to perform floating-point division, which includes the fractional part. This behavior is a result of C++ operators being sensitive to operand types and performing appropriate conversions as needed.

In C++, there are two floating-point data types: double and float.

%%cpp
constexpr auto PI = 0.314e1  // scientific notation

The qualifier constexpr declares a constant expression whose value is known at compile time and will not be modified later. In comparison, the qualifier const can declare a runtime constant whose value need not be determined at compile time.[9] For instance, a number randomly drawn with std::rand() from <cstdlib> can be defined as a constant, but not a constant expression:

%%cpp
const auto a=rand();
a

The following will fail because the initializer is randomly drawn at runtime:

%%cpptutor
int main() {
    constexpr auto a=rand();
    return 0;
}

To enter a float, add the suffix f to the floating point number:

%%cpp
constexpr auto PI = 3.14f  // single precision instead of the default double precision
%%cpptutor
int main() {
    auto x = 0f;
    return 0;
}
Solution to Exercise 10

This is because 0 is not a floating point number since it does not have the point. 0.f or 0e1f would work.

Precision

The benefit of float is that it occupies less memory than double:

%%cpp
sizeof(double)
%%cpp
sizeof(float)

However, the smaller memory footprint comes at the cost of a lower precision. For instance, consider the mass-energy equivalence:

%%cpp
constexpr float c = 2.99792458e8f;  // the speed of light
float m = 5.0f, E = m*c*c;        // the mass-energy equivalence

Of course, E=mc2E=mc^2 as verified below:

%%cpp
(E == m*c*c)

But Ec2≠m\frac{E}{c^2} \neq m somehow:

%%cpp
E/(c*c) == m

Changing the order of operations seems to fix it:

%%cpp
E/c/c == m

Using double instead of float also works:

%%cpp
constexpr double c = 2.99792458e8;
double m = 5, E = m*c*c;
E == m*c*c && E/(c*c) == m

You might think that the issue has to do with very large/small numbers. The following shows that even a number close to 1, and with just one decimal place cannot be accurately represented in floating point:

%%cpp
cout << fixed << setprecision(20) << 1.1 << '\n';  // for double

Similarly for Python:

print(f"{1.1:.20f}")

Similarly for float:

%%cpp
cout << format("{:.20f}\n", 1.1f);  // for float

Floating-point numbers have limited precision:

  • Single precision is accurate typically to 6-9 decimal digits.

  • Double precision is accurate typically to 15-17 decimal digits.

The limits of float (and similarly double) can be obtained as follows from numeric_limits:

%%cpp
cout << format("Minimum value: {}\n", std::numeric_limits<float>::min());
cout << format("Lowest value (including subnormal): {}\n", std::numeric_limits<float>::lowest());
cout << format("Maximum value: {}\n", std::numeric_limits<float>::max());
cout << format("Epsilon (difference between 1.0 and the next representable float): {}\n", std::numeric_limits<float>::epsilon());
cout << format("Round error: {}\n", std::numeric_limits<float>::round_error());
cout << format("Denormalized minimum value: {}\n", std::numeric_limits<float>::denorm_min());
%%cpp
numeric_limits<float>::min()           // the smallest positive normal value
%%cpp
numeric_limits<float>::lowest()        // the lowest finite value
%%cpp
numeric_limits<float>::max()           // the largest finite value
%%cpp
numeric_limits<float>::epsilon()       // the gap from 1.0 to the next value
%%cpp
numeric_limits<float>::round_error()   // the maximum rounding error

There are also some special values defined according to the IEEE 754 standard:

%%cpp
numeric_limits<float>::infinity()      // the positive infinity value
%%cpp
numeric_limits<float>::quiet_NaN()     // a quiet NaN value
%%cpp
numeric_limits<float>::signaling_NaN() // a signaling NaN value
%%cpp
numeric_limits<float>::denorm_min()   // the smallest positive subnormal value

Python’s float is different from C++'s float because it has double precision:

import sys
sys.float_info

To understand the precision issue of floating point numbers, play with a simulator:

The following is an IEEE 754 simulator written in Python:

@interact(x=FloatLogSlider(
    value=1,        # Initial value of the slider
    base=2,         # Base of the logarithm (e.g., 10 for base-10 log)
    min=-1023-52,
    max=1023,
    step=1,
    description='x' # Label for the slider
))
def double2binary(x):
    # Convert the double to its binary representation
    binary = f"{unpack('>Q', pack('>d', x))[0]:064b}"
    
    # Extract sign, exponent, and mantissa
    sign = binary[0]
    exponent = binary[1:12]
    mantissa = binary[12:]
    
    # Convert exponent and mantissa to decimal
    sign_val = int(sign, 2)
    exponent_val = int(exponent, 2)
    mantissa_val = int(mantissa, 2)/2**52
    
    # Create color-coded HTML output 
    html_output = (
        f"Binary: "
        f"<span style='color:red;'>{sign}</span>"
        f"<span style='color:green;'>{exponent}</span>"
        f"<span style='color:blue;'>{mantissa}</span><br>"
    ) 
    html_output += (
        f"$(-1)^{{\\color{{red}}{sign_val}}}\\times "
        f"2^{{{{\\color{{green}}{exponent_val}}}-1023}}\\times "
        f"(1+{{\\color{{blue}}\\text{{{mantissa_val}}}}})$"
    ) if exponent_val < 2047 else (
        r"NaN" if mantissa_val > 0 else (
            f"${('', '-')[sign_val]} \\infty$"
        )
    )

    display(HTML(html_output))
double2binary(float('inf'))
double2binary(-float('inf'))
double2binary(float('nan'))
%%cpp
const double m=1e16;
m - 1 == m
%%cpp
const double m=1e100;
m*m*m*m == m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m
Solution to Exercise 11
  1. m + 1 remains the same as m because 1 is smaller than the smallest increment to 1e16.

  2. m*m*m*m is equal to m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m because both are represented as inf.

%%cpp
constexpr float mass_of_universe = 1.45e53;
constexpr float num_of_atoms = 1e80;
const float mass_of_atom = mass_of_universe/num_of_atoms;
(mass_of_atom == mass_of_atom)
Solution to Exercise 12

mass_of_universe and num_of_atoms exceed the maximum value of float, and so they are regarded as infinity ∞\infty in float, denoted as inff. Thus, the mass of an atom is calculated as the ratio ∞∞\frac{\infty}{\infty}, which is undefined and denoted as nanf. However, nanf is not equal to anything including nanf itself, according to the standard.

Scope

The access of a variable is restricted to its scope. For instance, in Program 1, a and b can be accessed anywhere since they have global scope, but c can only be accessed within the compound statement enclosed by the braces { ... }, which creates a block scope.

{
  ...
  int c = b;
  ... // c visible here
} // c is out of scope

C++ follows lexical scoping to access variables defined in the closest enclosing scope. To understand how this works, consider the following example:

%%cpp
// global scope
int a;
{ // block scope level 1
    { // block scope level 2
        int a;
        { // block scope level 3
            cout << "Level 3: a=" << a << '\n';
        }       
    }
    cout << "Level 1: a=" << a << '\n';
}
  • The first cout in level 3 accesses a defined in level 2, which shadows the variable a in the global scope. Note that local variables are not initialized to 0 by default.

  • The second cout in level 1 accesses a defined in the global scope, which is initialized to 0 by default. a defined in level 2 is out of the scope of level 1.

In C++, a variable is not merely a name; it is a named container whose size is determined by its type. In comparison, Python is dynamically typed. Instead of a memory location, a variable in Python can be considered simply as a name of an object. In particular, the memory locations of different variables can be the same:

a = b = 1
print(f"a={a} @ {id(a):#x}")
print(f"b={b} @ {id(b):#x}")

The above uses id in CPython, which returns the memory location of its argument. The assignments above are called aliasing, since both a and b are different names pointing to the same memory location.

For C++, different variables normally have different memory locations even if they have the same value.

%%cpp
int a=1, b=a;
cout << format("a={} @ ", a) << &a << '\n';
cout << format("b={} @ ", b) << &b << '\n';

The above code uses the address-of operator &, which returns the address of type int *:

%%cpp
&a

Variables with the same name also have different memory locations:

%%cpp
int a=1;
{
    int a=++a;
    cout << format("a={} @ {:p}\n", a, static_cast<void*>(&a));
}
cout << format("a={} @ {:p}\n", a, static_cast<void*>(&a));

We can store the address using a variable known as a pointer:

%%cpp
int* p=&a;
cout << format("a={}\n", *p);

*p above uses the indirection/dereference operator * to access the value that p points to. Indeed, since it is far more common to operate on *p instead of p, the declaration for multiple pointers requires specifying * for each pointer:

%%cpp
int *p=&a, *q=&b;
cout << format("a={}, b={}\n", *p, *q);

The default initialization for pointers with static storage duration is nullptr or 0, referred to as the null pointer. The value indicates that the pointer does not point to any object, i.e., dereferencing it leads to an error:

%%cpp
int *p, *q=0, *r=nullptr;  // global p is initialized to nullptr by default
p==q && q==r               // nullptr has an integer value 0
%%cpp
{
    int *p;
    cout << *p;
    // *p = 1; // 😈: If you never try, you'll never know.
}

The use of pointers can get rather complicated as can be seen in Program 3. The following code uses the idea to give an IEEE 754 simulator in C++:

%%cpp
double x=1.;
auto i = reinterpret_cast<int64_t *>(&x);
cout << format("{:064b}", *i);

The above prints the binary representation of the integer i, which is also the binary representation of the floating point number x.

The effect of aliasing can also be achieved using pointers.

%%cpp
int a=1, *b=&a;
cout << format(" a={} @ {:p}\n", a++, static_cast<void*>(&a));
cout << format("*b={} @ {:p}\n", *b, static_cast<void*>(b));

Aliasing can also be achieved in C++ using an lvalue (locator value) reference such as int &:

%%cpp
int a=1, &b=a;
cout << format("a={} @ {:p}\n", a++, static_cast<void*>(&a));
cout << format("b={} @ {:p}\n", b, static_cast<void*>(&b));

The increment a++ also modifies the value of b because the declaration int &b=a; binds the address of a to b, so both a and b share the same memory location. The behavior of aliasing in Python is still very different:

b = a = 1  # a and b have the same memory location
a = a + 1  # a incremented to 2
b          # b is also 2? a and b have the same memory location right?

In C++, an integer such as 1 also has an associated memory location, but it is temporary and optimized heavily by the compiler in a way not suitable for users to modify. int &b=1; fails because, otherwise, the location of 1 would be exposed to the users to modify. We say that 1 is not an lvalue, but rather, an rvalue, or more specifically, a prvalue.

It is okay, however, if b is declared as a constant:

%%cpp
const int &b=1

The compiler can safely extend the lifetime of the prvalue as long as b is in scope, without concern about modifications or interference with optimizations for temporary objects. Such a constant reference will be useful when passing larger objects around without the extra copy step as in the copy assignment.

%%hermes
Explain briefly how pass by constant reference works in C++.
Solution to Exercise 13

This is because ++a returns an lvalue reference, but (a++) returns an rvalue, namely, the increment of the value of a.

%%hermes
Explain the value categories of C++ with concrete examples.
Solution to Exercise 14

No. Inside the block {}, a new variable a (b) is declared and then initialized. At the time of initialization, the new variable a (b) is already declared so it shadows the global variable a (b). However, the new variable a (b) is not yet initialized, so its value is indefinite.

As a hint, try running the following program to check whether the local variables are actually clones of the global variables:

%%cpp
int a=2*3*4, b=3*4*5;
{
    int a=a, b=b;
    cout << format("a={}\nb={}\n", a, b);  // local clone of global a and b?
}
cout << format("a={}\nb={}\n", a, b);  // global a and b not overwritten

String

How to represent a piece of text, which consists of a sequence of characters?

A string literal is delimited by double quotes:

%%cpp
"15"

Note that the data type is not string but const char[3]: a constant array of 3 char values. Such an array of characters is referred to as a C string, even though there isn’t actually a distinct cstring data type in C.

Why are there 3 characters instead of 2? It’s almost as if there’s a hidden element at play. We can inspect the elements of the array using a member access operator like [], but what secrets might it unveil?

%%cpp
"15"[0] // [0] picks out the first character at index 0
%%cpp
"15"[1] // the second character

Lo and behold 🪄:

%%cpp
"15"[2] // the third character

The last character is the null character, which can also be entered as '\0'.

%%cpp
'\0'

C strings are null-terminated to ensure they are (uniquely) decodable. In other words, the null character signals the end of a string without needing to keep track of its length. What a clever mechanism!

Can you print a string containing \0 in the middle?

%%cpp
cout << "321\0 123";
%%cpp
cout << "321\0123";
Solution to Exercise 15
  • The first code prints 321 because \0 terminates the string.

  • The second code prints 321 in one line followed by 3 in the second line because \012 is the octal representation of the newline character.

%%cpp
"15"[3]
Solution to Exercise 16

C++ does not keep track of the length of C string at runtime. It uses the null character to signal the end of a string.

The length of a C string can be returned using std::strlen from <cstring>:

%%cpp
strlen("15")

Note that the return type is std::size_t, which is also defined in other header files such as <cstddef>. Failure to understand the implication could lead to logical error like the following:

%%cpp
strlen("Ava") - strlen("Betty") > 0
%%hermes
In C++, should `strlen("Ava") - strlen("Betty") > 0` return false since 
"Ava" is shorter than "Betty"?
Solution to Exercise 17

The maximum possible length is given by the maximum value of size_t, which is numeric_limits<size_t>::max(). The memory and storage available are also limiting factors.

%%hermes
Since the length of a C string is recorded in its type, why do we need to 
null-terminate it? Isn't it a waste of memory to store the null character?

Another frustrating gotcha is that the length of a string is not the same as the number of characters or symbols in the string! Consider the length of “思”:

%%cpp
strlen("思")

Why is the length 3 instead of 1?

%%cpp
"思"[0]
%%cpp
"思"[1]
%%cpp
"思"[2]

Finally:

%%cpp
"思"[3]

The three characters before the null terminator are actually the three bytes that correspond to the UTF-8 encoding of “思”. UTF-8 is a variable-length encoding designed to represent Unicode characters, including those that go beyond the ASCII character set.

For example, the emoticon below requires more bytes to encode:

%%cpp
"😎"

A unicode symbol can also be encoded differently using UTF-16:

%%cpp
u"思"
%%cpp
u"思"[0]
%%cpp
u"思"[1]
%%cpp
"\u601D" // escape code to enter a symbol using UTF-16 code
%%hermes
In C++, how to return the number of symbols in a C string, given that the
symbols may be encoded using a variable-length code.

There are other types of string literals in C++, but their support varies. For example, strlen(u"1") or cout << u8"1"; will fail because these functions and operators do not handle wide or UTF-8 encoded strings directly. The exception is raw string literals, which ensure WYSIWYG (to some extent):

%%cpp
cout << R"(
  ____  ____   ____   _____  _   ___  
 / ___|/ ___| |___ \ |___ / / | / _ \
| |    \___ \   __) |  |_ \ | || | | |
| |___  ___) | / __/  ___) || || |_| |
 \____||____/ |_____||____/ |_| \___/ 😎
)";

What does it mean to be a constant array?

Consider the following code:

%%cpp
auto s="15";
s = "35";       // works
// s[0] = '1';  // fails
%%cpp
auto &s="15";
Solution to Exercise 18

The binding fails for auto &b=1 because auto is deduced to be the non-constant int from 1. The binding succeeds for auto &s="15" because auto is deduced to be the constant pointer type const char *const from "15". Since s is a constant, further assignment will fail.

Footnotes
  1. For instance, two numbers with GCD equal to 1 are called coprime, which is a concept used in finite fields with significant applications to different areas such as coding theory and cryptography.

  2. sizeof is not a function, even though its usage—such as sizeof(long long)f—resembles a function call. In C++, You cannot pass a type as an argument to a regular function. For template functions, types can be passed but using angle brackets instead of parentheses.

  3. Bitwise operators perform operations on the bits of the binary representation of integers. You may explore other bitwise operators to learn about them:

    1. Bitwise Shift: <<, >>

    2. Bitwise AND: &

    3. Bitwise XOR: ^

    4. Bitwise OR: |

  4. This will be useful when defining a recursion using lambda expression.

  5. The exception is the assignment expression using the walrus operator :=.

  6. The type double will be explained in a later section on floating point number.

  7. The address-of and dereference operators will be explained in a later section on variable scope.

  8. Modern CPUs and GPUs are equipped with dedicated hardware called Floating Point Units (FPUs) specifically designed to handle floating-point arithmetic efficiently.

  9. Constant expressions potentially allows the compiler to further optimize the code for faster execution.