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++.
To specify and store the non-zero integers and :
%%cpp
int a=2*3*4, b=3*4*5; // integer variables declaration and initialization
cout << format("gcd({}, {})=?\n", a, b); // print formatted stringTo compute the GCD of and step-by-step, repeatedly run the following program until
abecomes0, in which case the absolute value ofbis 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 statementSolution 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 iterations for and for because
In terms of computational complexity, Euclidean algorithm is quite efficient . 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?Hermes
There is no single “fastest” GCD — it depends on the inputs. The practical answer for machine-sized integers is Stein’s binary GCD (1961), which avoids the expensive division and uses only bit shifts, subtractions, and comparisons. For very large (big-integer) inputs, Lehmer’s / half-gcd algorithms are asymptotically faster.
Euclid’s algorithm (the lecture’s approach) is already optimal in the number of iterations: it takes steps, and the worst case is exactly consecutive Fibonacci numbers — the case analyzed in Exercise 2. Each step, however, performs a division, the slowest integer instruction the CPU decodes.
Binary GCD produces the same answer but never divides:
It strips out all common factors of 2,
strips the remaining factors of 2 from one operand, then
repeatedly replaces the larger of the two odd numbers with their difference.
Every operation is a cheap shift or subtract, which is why it is typically the fastest GCD in practice and what many libraries use.
long long gcd_binary(long long a, long long b) {
if (a == 0) { return b; }
if (b == 0) { return a; }
int shift = 0;
while (((a | b) & 1) == 0) { a >>= 1; b >>= 1; ++shift; } // common 2s
while ((a & 1) == 0) { a >>= 1; } // 2s out of a
do {
while ((b & 1) == 0) { b >>= 1; }
if (a > b) { long long t = a; a = b; b = t; }
b -= a;
} while (b != 0);
return a << shift;
}Design notes:
No division at all — the correctness rests on the identity that and when is odd, so factors of 2 can be deferred and shifted back at the end.
Same complexity class, smaller constant — still iterations, but each iteration is a cheap shift/subtract instead of a division, so the measured runtime is far lower (see the timing cell above).
gcd_euclidis kept for comparison so you can see both return identical values on the Fibonacci worst case.
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
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++.
Store the real number approximately as a floating-point number, and compute its reciprocal 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);To compute the desired square root of the reciprocal, repeatedly runs the following until the answer remains unchanged/steady.
1y = (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 from as :
%%cpp
1/y/yThe program can be improved by:
A better initial guess
1 2 3auto 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 guessAn alternative update rule
1y = 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/ySolution 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
while the second one Program 4 is obtained by thinking of the answer as a root of the function
In Program 3 auto i = *reinterpret_cast<int64_t *>(&x);,
&xobtains the address ofx,reinterpret_cast<int64_t *>converts it to the pointer for a 64-bit integer typeint64_t, andauto i = *...reads the binary representation of the floating-point numberxas the binary representation of an integer, and store it asi.auto y = *reinterpret_cast<double *>(&i);reads the integer bits ofiback to the floating-point bits and store it asy.
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.Hermes
Computing with only , , , (and comparisons) is a classic. The lecture already met one such method in disguise — the fast inverse-square-root update rule is just Newton’s method applied to . The three methods below are its companions; all three use nothing but the four basic operations.
1. Bisection (false position, “halve the bracket”). Keep a range with ; put , and if the root is in , else in . Repeat until the bracket is tiny. Uses only a multiply and a compare per step ( vs ) — no division. Converges linearly (one extra correct digit per step), so it is reliable but slow.
2. Newton–Raphson (the Babylonian / “heron” method). Solve ; Newton’s step with is exactly
It needs a division and an average per step, and it converges quadratically: the error roughly squares each iteration, so 4–5 steps give full double precision from a crude start. This is the same update family as the lecture’s , which is Newton on . In practice this is the family hardware and libraries build on.
3. Digit-by-digit (“schoolbook” / long-division) extraction. The manual method: group the digits in pairs from the decimal point; at each step, with current answer and remainder , find the largest digit such that . It is the square-root analogue of long division and uses no division at all in the digit search — only , , and comparisons — which is exactly why it was the go-to method before scientific calculators.
The cell above runs all three on the lecture’s (plus a digit check on 1056) and prints Newton’s converging steps.
Design notes:
Same answer, different speeds. Bisection and Newton both find ; Newton reaches full precision in ~5 iterations where bisection would need ~50. The digit method is exact for perfect squares and gives otherwise.
Newton is the bridge to the lecture. Its update for is the same contraction, just targeting the reciprocal — which is why a good initial guess (the famous bit-trick) matters so much there.
All arithmetic, no
sqrt(). The whole point is these reproduce the result of the library call using only the basic operations the CPU is guaranteed to have.
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 hexadecimalAll 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, 0xFC++ also supports different types of integer literals of different sizes, e.g., 3 billions is represented as type long:
%%cpp
30000000001019 is represented as unsigned integer type:
%%cpp
10000000000000000000 // with a warningWe 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)*8Shouldn’t we use cout << instead to print the number of values?
cout << instead to print the number of values?The number of values of type int can be computed using the bitwise shift operator <<, which is not the usual stream insertion operator used to print to standard output/error cout/cerr:[3]
What happens if you change 1uLL to 1?
1uLL to 1?The computed value will be wrong. You may see a warning such as
warning: shift count >= width of type [-Wshift-count-overflow]because int is not large enough to represent the number of values of type int! Can you explain what the computed value actually is? Try running the code in xeus-cpp kernel as a comparison.
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?Hermes
First, a small correction to the question: the C++ standard never requires int to be 16 bits — it requires it to be at least 16 bits. It also only requires an ordering, not exact widths: sizeof(short) ≤ sizeof(int) ≤ sizeof(long), with short at least 16 bits, int at least 16 bits, and long at least 32 bits. Within those floors, the width of int is implementation-defined — the compiler picks it for the target machine. The cell above prints this machine’s actual widths (16/32/64/64 here) and checks the ordering.
So int is 32 bits on this machine for three reinforcing reasons:
intis meant to be the native word size. The whole design is thatintis “the integer the hardware handles best.” On a 32-bit CPU (the x86 era) the natural word is 32 bits, sointis 32. On today’s 64-bit machines the hardware word is 64, but theint/long/pointer layout is frozen by the data model of the platform, not by the language:ILP32 (32-bit systems):
int=long= pointers = 32 bits.LP64 (Linux/macOS x86-64, ARM64):
longand pointers = 64, butintstays 32.LLP64 (Windows x86-64):
long longand pointers = 64,longstays 32,intstays 32. In every modern modelintis 32; that’s why you see 32 even on 64-bit machines.
Backward compatibility. If
intsuddenly doubled to 64 bits, every loop counter, array index, and binary layout written for the 32-bit world would change size, break the ABI, and change how much memory programs use. Keepingintat 32 lets decades of code run unchanged — which is exactly the portability C and C++ were built to give.The minimum exists for tiny machines. The “at least 16” floor is what lets the same source compile on 16-bit microcontrollers (where
intis 16) and on big machines. On such embedded targets,intlegitimately is 16 bits — the standard’s floor is met, nothing more. That’s precisely why the next cell recommends fixed-width types (int16_t,int32_t,int64_tfrom<cstdint>) when you actually need a guaranteed size across platforms.
In short: 32 bits is not something the standard demands — it’s the value the implementation chose because 32 is the native, historically-stable word size for int on every common platform, and it happens to sit comfortably above the standard’s 16-bit floor.
Design notes:
The ordering is the real guarantee.
short <= int <= longis what the standard actually pins down; the 16/32/64 numbers are lower bounds, and on real systems the types are typically exactly 16 / 32 / 32-or-64.Don’t write “32” where it matters — if code depends on the width, use
int32_t(and friends); if it just needs “a normal integer,”intis the right, idiomatic choice.
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 << 16For 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
dNote 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;Hermes
None of the three, guaranteed. int one = one + 1; is undefined behavior — the one on the right-hand side is the same variable being declared, and it has no value yet when the initializer runs. You’re reading it before it’s initialized.
Why none of one+1, 1, or 0 is a correct answer:
Not
one+1— there is no earlieroneto add to; the name doesn’t refer to any existing object here.Not
0by rule — it’s not “zero-initialized” like a static; an automatic variable with no initializer just holds indeterminate (garbage) bytes.Not
1by rule either — on this machine, in this cell, the garbage happened to be0, so0 + 1printed1. That’s luck, not the language.
The cell above makes this concrete: g++ warns ‘one’ is used uninitialized (-Wuninitialized), and the printed value depends on whatever was already in that stack slot. A different compiler, optimization level, or a few instructions earlier could print anything — 1, -1, a huge number, etc. The only thing you can say is that the read is undefined behavior, so any value (or a crash) is “correct.”
That’s also why the note says auto would not work: auto one = one + 1; can’t deduce a type from an initializer that itself refers to the very variable being deduced.
Design notes:
Point of declaration — a variable is only usable from after its full declaration is done; the initializer still belongs to the declaration, so
oneisn’t in scope with a value until the initializer is finished. Reading it there is illegal.The sibling cell
int one(one+1);is the same trap written in function-style (direct) initialization — also undefined behavior, also flagged by-Wuninitialized.
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);
oneLet’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?
dTo catch bugs caused by narrowing, C++11 introduced list initialization:
%%cpp
int one{one+1};
oneThis 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++?
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.
In C++, an assignment operation has a value equal to the assigned value, so
(d += 1)evaluates tod+1;(e += d+1)evaluates toe+d+1; and(f += e+d+1)evaluates tof+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 == zSolution to Exercise 6
The chaining in C++ works differently than the chained comparison in Python.
x == y == zevaluates totrue == zsince==is left-associative andx == yevaluates totrue; andtrue == zevaluates tofalsesincezis0, 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 follows that of the dividend (divisor) for C++ (Python). E.g., for C++,
The quotient 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.Hermes
There is no single “correct” rule for what should return when the result is not an integer — you must drop part of the fractional part, and there are two natural ways to do it. C++ and Python simply chose different ones, for different reasons.
Both languages agree on the identity with . What differs is which integer lands on, and the remainder is forced to follow:
C++ truncates toward 0 (round down in magnitude). The remainder then carries the sign of the dividend — so and .
Python floors toward (round down to the next smaller integer). The remainder then carries the sign of the divisor — so and .
Why they differ — the design motivation:
C++ reflects the hardware. On the machines C++ targets, integer division is a single
idivinstruction that truncates toward zero (it keeps the top word of the quotient). The language exposes that behavior directly, soa/banda%bshare the dividend’s sign and the two operations stay consistent with one instruction.Python prioritises mathematical and modular convenience. Floor division makes the remainder always non-negative when the divisor is positive (), which is exactly the textbook definition of a residue and what you want for indexing, wrapping, and modular arithmetic — e.g. is the same residue class as 1, so is ready to use as a 0-based index without a sign fix-up.
In short: C++ optimises for the machine (truncate, match idiv); Python optimises for the maths (floor, non-negative residues). The code cell above reproduces both rules with the same two-line identity and shows the results line up with the lecture’s 5/3, -5/3, 5/-3, -5/-3 examples.
Design notes:
div_pyis implemented on top of the C++ divide: take the truncating quotient, then step down once more if the remainder’s sign disagrees with the divisor’s — the floor is at most one unit away from the truncation.a == q*b + ris printed for both and is always1(true): both schemes are valid Euclidean divisions, they just split the quotient/remainder differently.
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 * 2char can also be signed or unsigned:
%%cpp
numeric_limits<char>::min() // sign bit (left-most bit) equals 1%%cpp
numeric_limits<unsigned char>::min() // 0A 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.Hermes
C++11 replaced the C-style cast with four named casts, each doing one thing with a compile-time check. The cell above exercises all of them (the lines are its real g++ output, not a sample).
static_cast— the workhorse: reasonable value conversions (numeric, or up/down casts in a class hierarchy).int n = static_cast<int>('A');→65.const_cast— the only legal way to add or removeconst/volatile.int* pc = const_cast<int*>(&cv);(forming the pointer is fine; writing through it is undefined behavior).reinterpret_cast— low-level re-interpretation between unrelated types, or pointer ↔ integer. Same bytes, new type.int64_t bits = reinterpret_cast<int64_t&>(dd);is the bit-trick behind the fast inverse square root from earlier.dynamic_cast— the safe down/cross-cast in a polymorphic hierarchy. It checks at runtime and returnsnullptron failure (throwsbad_castfor references).dynamic_cast<D*>(&plain)→nullptrbecause aBis not really aD.C-style cast
(int)'A'— the legacy kitchen sink: it silently picks whichever of the four above fits, with none of the type-safety checks. That is why the lecture prefers the named casts; a(T*)pointer-cast thatstatic_castwould reject quietly becomes areinterpret_castin disguise.
The one-line mapping: static_cast = value/type you mean; const_cast = drop const; reinterpret_cast = same bits, new type; dynamic_cast = is this really a subclass?; (T)x = all of the above, unchecked.
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) // worksSolution 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);Hermes
Nothing is truncated by the C++ program — it writes all 26 bytes of the formatted string. The display is cut short because the string contains a NUL byte.
The cell above is executed in the cell just above — its hex dump is its real output, not a sample. Formatting static_cast<char>(0) inserts the NUL character (byte value 00) into the string, right after "The ASCII code of ":
The formatted string has 26 bytes:
54 68 65 20 41 53 43 49 49 20 63 6f 64 65 20 6f 66 20 00 20 69 73 20 30 2e 0aThat 00 at position 18 is the embedded NUL. The terminal / notebook display treats a NUL byte like the end of the text (it is how C-style strings are terminated), so the visible output stops after The ASCII code of — even though is 0. (bytes 69 73 20 30 2e 0a) was actually written to stdout after it. The same thing happens in the preceding lecture cell; it is not a bug in format.
Design notes:
charis an integer type, soformat("{}", c)accepts it and prints the character it denotes — including control characters such as NUL. For the value, format the character as an integer:(int)c0(orstatic_cast<int>(c)).The string object itself is complete (26 bytes here); only its presentation is cut at the NUL. This is why
static_cast<char>(128)“works” when used as an expression value (cell8df9631f) but printing it behaves differently — the value is fine, displaying it is the issue.
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,
%%cpp
22/7That 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 periodSolution 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 notationThe 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();
aThe 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 equivalenceOf course, as verified below:
%%cpp
(E == m*c*c)But somehow:
%%cpp
E/(c*c) == mChanging the order of operations seems to fix it:
%%cpp
E/c/c == mUsing 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) == mYou 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 doubleHow does the above code print up to 20 decimal places?
std::fixed: This manipulator sets the output format to fixed-point notation.std::setprecision(20)from<iomanip>: This sets the number of digits after the decimal point to 20.
Similarly for Python:
print(f"{1.1:.20f}")Similarly for float:
%%cpp
cout << format("{:.20f}\n", 1.1f); // for floatHow does the above code format the floating point number?
The code uses a format specifier {:.20f}, which is a little bit cryptic but very convenient.
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 errorThere 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 valuePython’s float is different from C++'s float because it has double precision:
import sys
sys.float_infoTo 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'))Why does the precision error depend on the order of operations?
Floating-point numbers operate at varying scales due to their exponents, so using the same number of bits for the mantissa can lead to precision errors that differ by scale. The order of operations affects how these errors accumulate, resulting in varying overall precision depending on the scale.
%%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*mSolution to Exercise 11
m + 1remains the same asmbecause1is smaller than the smallest increment to1e16.m*m*m*mis equal tom*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*m*mbecause both are represented asinf.
%%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 in float, denoted as inff. Thus, the mass of an atom is calculated as the ratio , 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 scopeC++ 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
coutin level 3 accessesadefined in level 2, which shadows the variableain the global scope. Note that local variables are not initialized to0by default.The second
coutin level 1 accessesadefined in the global scope, which is initialized to0by default.adefined 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
&aVariables 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));Shouldn’t the code prints a=2 @ ... first?
a=2 @ ... first?++a actually uses the already declared a in the block scope, whose value is uninitialized and therefore may not be 1.
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?Why b is not equal to 2?
b is not equal to 2?b is still 1 because it is not an alias of a, but rather, an alias of the object/integer 1.
a = a + 1in Python assign the nameato a new valuea + 1, without changing the value1thatbpoints to.a++in C++ increments the valuea, which is also the value ofb.
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=1The 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++.Hermes
Pass by constant reference declares a parameter as const T& — a read-only alias for the caller’s argument, not a copy. Why the const matters:
Read-only inside the function. The function cannot modify the caller’s object, and the contract is visible in the signature.
No copy. For a large object (e.g.,
const std::string&), binding a reference costs a handful of bytes as compared to copying the whole object. Pass by reference avoids the copy, andconstadds the safety of guaranteeing no modification.Rvalues are admissible. A
const T¶meter can bind to temporaries (as123did above), which is why library functions that only read their inputs declare them this way; a plainT&cannot bind to123at all.
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.Hermes
C++ gives every expression one of three value categories: lvalue, prvalue, or xvalue. The fundamental split is lvalue vs. rvalue: an lvalue names a region of memory that persists — it has identity (you can take its address and use it again) — while an rvalue is a value to be consumed. Rvalue is the umbrella term for the other two: a prvalue is a pure value with no identity of its own, and an xvalue is an expiring object (it keeps identity but is explicitly marked movable).
Part A — the lvalue / rvalue split, computed by overload resolution. category is overloaded on T& versus T&&: an lvalue argument binds T& (which is preferred), but an rvalue argument can only bind T&&, so the T&& overload catches exactly the rvalues. The output confirms the usual rules:
demo_a,s, ands[0]are lvalues — a variable and a subscript both name a region of memory.demo_a + 1, the temporarystd::string("world"), ands.size()(returned by value) are rvalues.std::move(demo_a)andstd::move(s)are rvalues —std::movemoves nothing by itself; it only casts to an rvalue reference.
Part B — rvalues split into prvalue and xvalue. A single function parameter cannot tell these two apart (both bind T&&), so rvalue_kind inspects decltype instead: an xvalue still refers to an object, so decltype(std::move(demo_a)) is int&&, whereas a prvalue is a pure value, so decltype(demo_a + 1) is int. Hence std::move(demo_a) is an xvalue but demo_a + 1 and the new string are prvalues.
Part C — the ++ asymmetry the lecture alluded to. ++c returns a reference to c, so it is an lvalue; d++ returns a copy, so it is a prvalue. That is precisely why (++c) = 1 compiles while (d++) = 1 does not — the real compiler rejects the latter with lvalue required as left operand of assignment (see Exercise 13).
Part D — the payoff: an xvalue enables a move. Binding the lvalue p to a ValueCatDemo&& parameter via std::move(p) turns it into an xvalue, so the move constructor runs and p is drained; copying the lvalue q runs the copy constructor instead. Real output:
p (prvalue in) / q (from xvalue) / r (from lvalue):
move constructor (from n=3)
copy constructor (from n=3)
p=0 q=3 r=3 (p was emptied by the move)In one line: an lvalue is an addressable object, an xvalue is an expiring object (the one that can be moved), and a prvalue is a value with no identity of its own (a literal, a computed result, a by-value return). It is what lets the compiler choose between copying and moving, and lets references bind to temporaries — the same reason a const int& was able to bind the literal 123 in the previous cell.
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 overwrittenString¶
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 characterLo and behold 🪄:
%%cpp
"15"[2] // the third characterThe 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
321because\0terminates the string.The second code prints
321in one line followed by3in the second line because\012is 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"?Hermes
The expression returns true, not false. This is an unsigned arithmetic trap. Let me read the surrounding cells for context.
Yes — intuitively it should be false ("Ava" is 3 chars, "Betty" is 5, so Ava is shorter). But in C++ it actually returns true. That mismatch is exactly the point of this cell (the markdown just above it calls this “a logical error” from misunderstanding the return type).
Why it’s true
std::strlen returns std::size_t, which is an unsigned integer. So the subtraction is done in unsigned arithmetic, and unsigned subtraction wraps around instead of going negative:
strlen("Ava") - strlen("Betty")
// 3 - 5 in size_t = 2^64 - 2 = 18446744073709551614That huge positive number is, of course, > 0, so the whole expression is true.
I verified it by compiling:
strlen("Ava") = 3
strlen("Betty") = 5
diff = 18446744073709551614
diff > 0 ? 1 // true <-- the buggy version
int version: 0 // false <-- if the operands were signedThe general trap
This is the classic unsigned wraparound bug. It shows up everywhere you compare two size_t values directly:
size_t a = 3, b = 5;
a - b > 0; // true, but a < b!How to write it correctly — compare the lengths directly, and never subtract two unsigned values expecting a negative:
strlen("Ava") > strlen("Betty"); // false, correct
// or, to express "Ava is shorter":
strlen("Ava") < strlen("Betty"); // trueSo your instinct is right about the intended semantics — the lesson is that C++ won’t give you that signed result automatically; the size_t return type silently turns your a - b > 0 idiom into a tautology. Compare the operands, don’t subtract-then-compare.
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?Hermes
First, a small correction: the length is recorded in the type only while the string is still an array. The literal "15" has type const char[3], and that 3 is known at compile time only. The moment the array is used as a pointer — which is what every string function actually receives — the type no longer records any length at all, and the null terminator is the only record of the length left at runtime. That is why it is needed.
Is the byte wasted? It is a constant one-byte overhead, and in exchange C gets a runtime string representation that is just a plain sequence of bytes: no header, no length field to keep in sync, no allocator, and the string can live in read-only program memory (which is exactly where literals do). The alternative design — storing the length alongside the characters, a length-prefixed string — is what std::string and most other languages’ string types use, and it can be more space-efficient. C deliberately traded that for a minimal, headerless representation in which any function can find the end of the string by scanning, with nothing to pass in besides the pointer.
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.Hermes
strlen counts bytes, not symbols — that is exactly why it returned 3 for "思" even though it is a single symbol. To count the symbols of a variable-length encoding, scan the byte sequence and use the structure of that encoding:
UTF-8 (
"思"): each symbol occupies 1–4 bytes. The first byte of a symbol is never a continuation byte (which has the bit pattern 0b10xxxxxx); all remaining bytes of the symbol are continuation bytes. Soutf8_symbolscounts the bytescwith(static_cast<unsigned char>(c) & 0xC0) != 0x80.UTF-16 (
u"思"): each unit is achar16_t(2 bytes). Most symbols occupy one unit, but a symbol outside the basic multilingual plane — such as 😎 — occupies a surrogate pair: a high surrogate (0xD800–0xDBFF) immediately followed by a low surrogate. Soutf16_symbolscounts the units, skipping the second unit of each pair.
The code above was compiled and run with g++ (-std=c++23 -O2) — the four lines below are its real output, not a sample:
"15": bytes=2, symbols=2
"思": bytes=3, symbols=1
"😎": bytes=4, symbols=1
u"思😎": code units=3, symbols=2The last line is the sharpest contrast: sizeof(u"思😎")/2 - 1 gives 3 (思 one unit + 😎 two units), while the number of symbols is 2 — the byte count with the terminator removed gets the answer wrong in both encodings for this string (7 vs 2 in UTF-8, 3 vs 2 in UTF-16), and only agrees by luck when every symbol is one byte (or one unit).
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'; // failsWhy is it okay to change all the characters as in s="35"; but not the first as in s[0]=1;?
s="35"; but not the first as in s[0]=1;?s is indeed not a constant. It is a pointer that points to characters in read-only memory.
s="35"is allowed since it changes the value of the pointer to point to a new array of characters.s[0]='3'is not allowed since it attempts to rewrite a character in read-only memory.
%%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.
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.
sizeofis 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.Bitwise operators perform operations on the bits of the binary representation of integers. You may explore other bitwise operators to learn about them:
Bitwise Shift:
<<,>>Bitwise AND:
&Bitwise XOR:
^Bitwise OR:
|
This will be useful when defining a recursion using lambda expression.
The exception is the assignment expression using the walrus operator
:=.The type
doublewill be explained in a later section on floating point number.The address-of and dereference operators will be explained in a later section on variable scope.
Modern CPUs and GPUs are equipped with dedicated hardware called Floating Point Units (FPUs) specifically designed to handle floating-point arithmetic efficiently.
Constant expressions potentially allows the compiler to further optimize the code for faster execution.