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

Instead of managing individual variables separately, composite data types enable developers to bundle related data together and access them conveniently. This concept is essential for modeling complex information and reducing redundancy. In this notebook, we will explore ordered sequences including fixed-length arrays and variable-length vectors in C++.

from __init__ import *
%reload_ext divewidgets
The history saving thread hit an unexpected error (OperationalError('attempt to write a readonly database')).History will not be written to the database.

Motivation

Composite data refers to structured groupings of multiple values under a single entity, allowing for more organized and efficient data handling. In this notebook, we will explore ordered sequences including fixed-length arrays and variable-length vectors in C++. Let us begin with some motivating examples.

Cross Product

Consider computing the cross product of two vectors, which has many applications in physics and computer graphics:

One way to implement the cross product is to use C-style fixed-size arrays:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * @brief Computes the cross product of two 3D vectors.
 *
 * This function calculates the cross product of two input vectors `u` and `v`,
 * each represented as a fixed-size array of 3 doubles. The result is stored in
 * the output array `w`, which must also be of size 3.
 *
 * @param u A constant reference to a 3-element array representing the first vector.
 * @param v A constant reference to a 3-element array representing the second vector.
 * @param w A reference to a 3-element array where the result (cross product) will be stored.
 * @example
 * double w[3];
 * cross((double[]){1,0,0}, (double[]){0,1,0},w); // (double[3]) { 0.0, 0.0, 1.0 }
 */
void cross(const double (&u)[3], const double (&v)[3], double (&w)[3]) {
    w[0] = u[1]*v[2] - u[2]*v[1];
    w[1] = u[2]*v[0] - u[0]*v[2];
    w[2] = u[0]*v[1] - u[1]*v[0];
}

Program 1:Compute the cross product u×v\M{u}\times \M{v} using C-style array.

%%cpp
void cross(const double (&u)[3], const double (&v)[3], double (&w)[3]) {
    w[0] = u[1]*v[2] - u[2]*v[1];
    w[1] = u[2]*v[0] - u[0]*v[2];
    w[2] = u[0]*v[1] - u[1]*v[0];
}
cross
(void (*)(const double (&)[3], const double (&)[3], double (&)[3])) Function @0x779283eda000
  at input_line_55:1:
void cross(const double (&u)[3], const double (&v)[3], double (&w)[3]) {
    w[0] = u[1]*v[2] - u[2]*v[1];
    w[1] = u[2]*v[0] - u[0]*v[2];
    w[2] = u[0]*v[1] - u[1]*v[0];
}

For instance, to compute w=u×v\M{w}=\M{u}\times \M{v} with

u=[100]v=[010]:\begin{align} \M{u}&=\begin{bmatrix} 1 & 0 & 0\end{bmatrix}\\ \M{v}&=\begin{bmatrix} 0 & 1 & 0\end{bmatrix}: \end{align}
%%cpp
double w[3];
cross((double[]){1,0,0}, (double[]){0,1,0},w);
w
(double[3]) { 0.0000000, 0.0000000, 1.0000000 }
Solution to Exercise 1

Returning w as follows does not work since return value optimization applies only to the pointer to the first element of w, but not the elements of w:

double* cross(const double (&u)[3], const double (&v)[3]) {
    double w[3];
    w[0] = u[1]*v[2] - u[2]*v[1];
    w[1] = u[2]*v[0] - u[0]*v[2];
    w[2] = u[0]*v[1] - u[1]*v[0];
    return w;
}

Declaring w as static double w[3]; is not recommended as the program will not be thread-safe unless w is a constant. It is also problematic to allocate w to the heap memory using auto w=new double[3]; as it requires programmers to manually clean up the storage to avoid memory leak. Passing w into the function circumvent the above issues since the caller of cross has complete control over how the memory should be allocated for w.

We’ll explore an alternative implementation of the dot product using C++11’s fixed-size container std::array from <array>, which can be safely and properly returned by a function similar to other basic data types.

Dot Product

Consider computing the dot product of two vectors, which measures how much one vector points in the direction of another. The dot product has numerous applications beyond physics and computer graphics, including statistics and machine learning.

To implement (5) properly with C-style fixed-size array:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * @brief Computes the dot product of two arrays of doubles.
 *
 * This function calculates the dot product of two fixed-size arrays `u` and `v`.
 * It assumes both arrays have the same length `n`.
 *
 * @param u A reference to an array of doubles.
 * @param v A reference to an array of doubles.
 * @param n The number of elements in each array.
 * @return The dot product of arrays `u` and `v` as a double.
 *
 * @example
 * double dp = dot((double[]){1,1,1,1}, (double[]){1,1,1,1}, 4); // 4.0
 */
auto dot(const double (&u)[], const double (&v)[], const size_t n) {
    auto prod = 0.;
    for (size_t i = 0; i < n; ++i) 
        prod += u[i] * v[i];
    return prod;
}

Program 2:Compute the dot product u⋅v\M{u}\cdot \M{v} using C-style array.

Note that the input vectors u\M{u} and v\M{v} are not restricted to the three-dimensional real vector space, i.e., n=3n=3.

%%cpp
auto dot(const double (&u)[], const double (&v)[], const size_t n) {
    auto prod=0.;
    for (size_t i=0; i<n; ++i) prod += u[i]*v[i];
    return prod;
}
cout << dot((double[]){1,0}, (double[]){0,1}, 2) << ' ';
cout << dot((double[]){1,0,0}, (double[]){0,0,1}, 3) << ' ';
cout << dot((double[]){1,0,0,0}, (double[]){0,0,0,1}, 4) << ' ';
cout << dot((double[]){1,0,0,0,0}, (double[]){0,0,0,0,1}, 5) << ' ';
cout << dot((double[]){1,0,0,0,0,0}, (double[]){0,0,0,0,0,1}, 6) << ' ';
0 0 0 0 0 

The above computes the dot products of orthogonal vectors in Rn\mathbb{R}^n for different dimensions nn. However, the dimension needs to be passed into the function as a separate parameter n.

To implement the norm in (6):

%%cpp
auto norm(const double (&v)[], const size_t n) {
    return sqrt(dot(v, v, n));
}
cout << norm((double[0]){}, 0) << ' ';
cout << norm((double[1]){1}, 1) << ' ';
cout << norm((double[2]){1,1}, 2) << ' ';
cout << norm((double[3]){1,1,1}, 3) << ' ';
cout << norm((double[4]){1,1,1,1}, 4) << ' ';
0 1 1.41421 1.73205 2 

This calculates the norm of the vectors of all ones in Rn\mathbb{R}^n for different dimensions nn.

Solution to Exercise 2

To allocate the memory statically for v, its size n must be a compile-time constant.

We will learn to use the C++11 container std::vector from <vector>, whose length can be changed dynamically.

Compound Literal

How to enter an array of elements?

Recall that a string is a constant array of characters:

%%cpp
"abc"
(const char[4]) "abc"

The size is fixed and can be obtained at compile time:

%%cpp
sizeof("Ava")  // in bytes
(unsigned long) 4

Instead of a string literal, we can enter a string as an array using the C-style compound literal:

%%cpp
cout << (const char[]){'A','v','a', 0};  // 😈: Oh, there is a typo... `, 0`.
Ava

We can also specify a bigger size for an array than the size of the initializer:

%%cpp
cout << (const char[4]){'A','v','a'}; // remaining entries initialized to 0
Ava

The entries of an array need not be characters but they must be of the same type:

%%cpp
(int[]){1,2,3}
(int[3]) { 1, 2, 3 }
%%cpp
sizeof((double[]){1,2,3})
(unsigned long) 24
%%cpp
sizeof((int[6]){1,2,3})  // `double` is not double the size of `int`?
(unsigned long) 24

This is unlike Python list and tuple, which can be heterogeneous, i.e., with elements of different types:

[(1, '2'), [False, 3]]
[(1, '2'), [False, 3]]

The C-style array is more like array from numpy, which must be homogenous, i.e., with elements of the same types:

import numpy as np

np.array([(1, '2'), [False, 3]])
array([['1', '2'], ['False', '3']], dtype='<U21')
%%hermes
Explain briefly the similarities and differences between numpy arrays and 
C-style arrays.
Loading...
🌿 Fork/new: main (will create on click)

An array in C++ can also be multi-dimensional, i.e., an array of arrays of arrays...:

%%cpp
(int[][2]){{1,2},{3, 4}}  // 2D array
(int[2][2]) { { 1, 2 }, { 3, 4 } }
%%cpp
sizeof((int[][2]){{1,2},{3, 4}})
(unsigned long) 16
%%cpp
(int[][2][3]){{{1,2,3},{4,5,6}},{{7,8,9}, {10,11,12}}}  // 3D array
(int[2][2][3]) { { { 1, 2, 3 }, { 4, 5, 6 } }, { { 7, 8, 9 }, { 10, 11, 12 } } }

The memory for a compound literal is statically allocated, and so the length must be fixed at compile time:

%%cpptutor
int main() {
    auto d=3u;       // mutable
    (int[d]){1,2,3}; // fails: `d` is not a constant
    return 0;
}
Loading...
%%cpp
constexpr auto d=3u;
(int[d]){1,2,3}   // works: `d` is a compile-time constant
(int[3]) { 1, 2, 3 }
%%cpptutor
int main() {
    const unsigned d=1+rand()%3;
    (int[d]){};    // fails: `d` is not known at compile time
    return 0;
}
Loading...
%%cpp
const auto d=3u;  // also a compile-time constant due to compiler optimization
(int[][d]){{1,2},{3, 4}}
(int[2][3]) { { 1, 2, 0 }, { 3, 4, 0 } }

Array Declaration

How to declare an array variable?

Consider the following character array:

%%cpp
(const char[4]){'a','b','c'}
(const char[4]) "abc"

The following somehow fails to assign it to a variable:

%%cpp
auto s = (const char[4]){'a','b','c'};
s
(const char *) "\x08" "F"
input_line_102:2:11: warning: temporary whose address is used as value of local variable 's' will be destroyed at the end of the full-expression [-Wdangling]
 auto s = (const char[4]){'a','b','c'};
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
%%cpp
auto s = "abc";
s
(const char *) "abc"

It also works if we bind the reference of the compound literal:

%%cpp
auto &s = (const char[4]){'a','b','c'};
s // the type of s is const char[4] instead of const char *
(const char[4]) "abc"

There are no issues if the compound literal is passed as an argument:

%%cpp
void print_s(const char * s) {
    cout << s << '\n';
}
%%cpp
print_s((const char[4]){'a','b','c'});
abc
%%hermes
Explain why the following fails to retain the value of the array:
auto s = (const char[4]){'a','b','c'};
but the followings can:
auto &s = (const char[4]){'a','b','c'};
auto s = "abc";

To declare an array of double, simply add the square brackets [] after the variable name to designate the size of the array:

%%cpp
int v[3];
v
(int[3]) { 0, 0, 0 }

Since v has a static storage duration, its elements are initialized to 0.0 by default.

An array can also be initialized by a form of list initialization called aggregate initialization:

%%cpp
int u[3]{1};
int v[]{0,1,0}, (&w)[3]=u;
// int v[];      // fails: needs an explicit size or an initializer
// int w[3]=u;   // fails: array initializer must be an initializer list. Why?
v
(int[3]) { 0, 1, 0 }

In the above declaration:

  • u[3]{1} initializes the first element as 1, and the subsequent elements are initialized as 0 by default.

  • v[] is an array of unknown bound. Such an array can be initialized with a non-empty list initializer such as {1,0,0}, from which the compiler can deduce how much space to allocate.

Solution to Exercise 3
  • int v[]; fails because it is missing an explicit size or an initializer for the compiler to determine how much space to allocate.

  • int w[3]=u; fails because an array initializer must be an initializer list. C-style array is not a first-class citizen as it is not directly assignable to a variable.

Array as Pointer

After an array has been declared, the same brackets [] are used as the member access operator to access or modify individual elements. E.g., the following function prints a 3D vector represented as an array of three elements:

1
2
3
void print_vector3_(const auto v[3]) {
    cout << format("[{}, {}, {}]\n", v[0], v[1], v[2]);
}

Program 3:Print a 3D vector represented as an array of three elements.

%%cpp
void print_vector3_(const auto v[3]) {
    cout << format("[{}, {}, {}]\n", v[0], v[1], v[2]);
}
print_vector3_((int[]){1,2,3});
[1, 2, 3]
%%cpp
{
    int u[3];
    print_vector3_(u);
}
[0, -700535568, 25847]
%%cpp
{
    int u[3] {};
    print_vector3_(u);
}
[0, 0, 0]
Solution to Exercise 4
  • The first cell does not initialize u because the declaration has no initializer, and u does not have static storage duration since it is declared in a block scope.

  • The second cell initializes the elements of u to 0 by default since an initializer list is given even though it is empty.

The definition of print_vector3_ is unsafe because it can cause buffer overrun:

%%cpp
print_vector3_((int[]){1,2});   // 😈: What is that last element...
[1, 2, -2124132280]

Even though the input array only has two elements, print_vector3_ prints the element after the second one, accessing an out-of-bound memory location!

void print_vector3_(const auto v[3]) {
    ... format(... v[2]);
}

If the input array is passed by reference instead, the size is enforced:

1
2
3
void print_vector3(const auto (&v)[3]) {  // pass by constant reference
    cout << format("[{}, {}, {}]\n", v[0], v[1], v[2]);
}

Program 4:Input array of three elements passed by reference.

%%cpp
void print_vector3(const auto (&v)[3]) {
    cout << format("[{}, {}, {}]\n", v[0], v[1], v[2]);
}
print_vector3((int[]){1,2,3});
// print_vector3((int[]){1,2});  // fails since double[3] is expected.
[1, 2, 3]
%%hermes
In the above code, why pass by reference works but pass by value doesn’t?

C++23 adds std::println in <print>, which formats an entire array or vector in a single call — no manual loops and no std::format.

%%cpp
#include <print>
%%cpp
std::println("{}", (int[]){1});
std::println("{}", (int[]){1,2,1});
std::println("{}", (int[]){1,3,3,1});
[1]
[1, 2, 1]
[1, 3, 3, 1]

std::println appends a newline automatically, and std::print is the variant without one.

%%cpp
std::print("{}, ", (int[]){1});
std::print("{}, ", (int[]){1,2,1});
std::print("{}", (int[]){1,3,3,1});
[1], [1, 2, 1], [1, 3, 3, 1]

1D Array

An array can be implicitly converted to a pointer that references the first element of the array:

%%cpp
int u[]{1,2,3};
*u  // first element
(int) 1

Indeed, the address of an array is the same as the address of its first element:

%%cpp
cout << format("&u : {:p}\n", static_cast<void *>(&u));
cout << format("&u[0] : {:p}\n", static_cast<void *>(&u[0]));
&u : 0x7791e04b3000
&u[0] : 0x7791e04b3000

Subsequent elements are stored in the subsequent locations:

%%cpp
cout << format("&u[1] : {:p}\n", static_cast<void *>(&u[1]));
*(u+1)  // second element
&u[1] : 0x7791e04b3004
(int) 2
%%cpp
cout << format("&u[2] : {:p}\n", static_cast<void *>(&u[2]));
*(u+2)  // third element
&u[2] : 0x7791e04b3008
(int) 3

The array can be assigned to a pointer or to another array by lvalue reference:

%%cpp
int u[]{1,2,3};
auto *v=u, (&w)[3]=u;
print_vector3_(v);
print_vector3(w);
[1, 2, 3]
[1, 2, 3]
%%cpp
cout << format("sizeof(u): {}\n", sizeof(u));
cout << format("sizeof(v): {}\n", sizeof(v));
(u==v)
sizeof(u): 12
sizeof(v): 8
(bool) true
Solution to Exercise 5

sizeof calculates the size of the array u at compile time because its length is known. The size is 12 because the array contains 3 int, each of size 4.

However, when u is assigned to a pointer v, the size information is lost — sizeof(v) only returns the size of the pointer itself (typically 8 bytes on a 64-bit system), not the size of the array it points to.

In other words, the subscript operation v[i] is equivalent to *(v+i) since elements of an array are stored in consecutive memory locations. What a clever trick! This implements an array efficiently without any overhead. Even the size of the array, which must be a compile-time constant, needs not be stored as meta data in the executable!

%%hermes
Explain briefly how memory is statically allocated to an array in C++, and 
whether the size of the array needs to be stored in the executable?

While an array can be assigned to a pointer, it cannot be assigned to an array:

double u[3]={1,2,3}, v[3];
v = u; // fails: array type 'int[3]' is not assignable
%%hermes
Explain briefly why a statically allocated array is not assignable?

When the array argument is passed by reference, the function signature is (void (*)(int (&)[3])) as expected.

%%cpp
void bar1(int (&v)[3]) {}
bar1
(void (*)(int (&)[3])) Function @0x7791e049e000
  at input_line_133:1:
void bar1(int (&v)[3]) {}

However, when the argument is passed by value, the function type is (void (*)(int *)):

%%cpp
void bar2(int v[3]) {}
bar2
(void (*)(int *)) Function @0x7791e049c000
  at input_line_134:1:
void bar2(int v[3]) {}

In other words, an array cannot be passed by value without decaying to a pointer. In particular, the size information is lost as the parameter decays from int v[3] to a pointer int *.

We can now answer the question:

Multi-Dimensional Array

A multi-dimensional array can also decay to a pointer in the same way. Consider the following 2-by-2-by-3 array:

%%cpp
int v[][2][3]{
    // v[0] or *v
    {
        // v[0][0] or **v
        {
            // v[0][0][0] or *(**v + 0)
            1,
            // v[0][0][1] or *(**v + 1)
            2,
            // v[0][0][2] or *(**v + 2)
            3
        },
        // v[0][1] or *(*v + 1)
        {
            // v[0][1][0] or *(**v + 3)
            4,
            // v[0][1][1] or *(**v + 4)
            5,
            // v[0][1][2] or *(**v + 5)
            6
        }
    },
    // v[1] or *(v + 1)
    {
        // v[1][0] or *(*v + 2)
        {
            // v[1][0][0] or *(**v + 6)
            7,
            // v[1][0][1] or *(**v + 7)
            8,
            // v[1][0][2] or *(**v + 8)
            9
        },
        // v[1][1] or *(*v + 3)
        {
            // v[1][1][0] or *(**v + 9)
            10,
            // v[1][1][1] or *(**v + 10)
            11,
            // v[1][1][2] or *(**v + 11)
            12
        }
    }
};
v
(int[2][2][3]) { { { 1, 2, 3 }, { 4, 5, 6 } }, { { 7, 8, 9 }, { 10, 11, 12 } } }
%%hermes
Why can we skip the bound of the first (instead of the last) dimension of a multi-dimensional array?
Solution to Exercise 6

This is because the array is runtime constant but not compile-time constant.

v defined above can be passed into a function with a parameter declared as int (&v)[2][2][3], int v[][2][3], or int (*v)[][3].

%%cpp
void foo1(int (&v)[2][2][3]) {}
foo1(v);
foo1
(void (*)(int (&)[2][2][3])) Function @0x7791e04a1000
  at input_line_136:1:
void foo1(int (&v)[2][2][3]) {}

%%cpp
void foo2(int v[][2][3]) {}
foo2(v);
foo2
(void (*)(int (*)[2][3])) Function @0x7791e0492000
  at input_line_137:1:
void foo2(int v[][2][3]) {}

%%cpp
void foo3(int (*v)[2][3]) {}
foo3(v);
static_cast<int (*)[2][3]>(v);
foo3
(void (*)(int (*)[2][3])) Function @0x7791e0490000
  at input_line_138:1:
void foo3(int (*v)[2][3]) {}

Solution to Exercise 7

The arrays v point to are not pointers, and they cannot be converted implicitly to pointers either, unlike the array v.

The 3D array v is actually stored like a 1D array **v in memory as shown below:

%%cpp
print_vector3(**v);
print_vector3(*(*v+1));
print_vector3(*(*v+2));
print_vector3(*(*v+3));
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[10, 11, 12]

To print all elements in one go:

%%cpp
for (auto l=0u; l<2*2*3; l++)
    cout << (**v)[l] << ' ';  // `(**v)[l]` is the same as `(&v[0][0][0])[l]`
1 2 3 4 5 6 7 8 9 10 11 12 

**v is the same as &v[0][0][0], i.e., the address to the first element of the 3D array.

This is equivalent to printing each element of the 3D array using nested for loops:

%%cpp
constexpr unsigned d[]{2,2,3};
for (auto i=0u; i<d[0]; i++)
    for (auto j=0u; j<d[1]; j++)
        for (auto k=0u; k<d[2]; k++)
            cout << v[i][j][k] << ' ';
1 2 3 4 5 6 7 8 9 10 11 12 

v[i][j][k] is the same as *(**v+i*(d[1]*d[2])+j*(d[2])+k):

%%cpp
for (auto i=0u; i<d[0]; i++)
    for (auto j=0u; j<d[1]; j++)
        for (auto k=0u; k<d[2]; k++)
            cout << *(**v+i*(d[1]*d[2])+j*(d[2])+k) << ' ';
1 2 3 4 5 6 7 8 9 10 11 12 
%%hermes
Explain the difference between F-ordering and C-ordering, and whether one is
better than the other.

Array Container

Program 1 implements the cross product using C-style fixed array:

void cross(..., ..., double (&w)[3]) {
    w[0] = ...
    w[1] = ...
    w[2] = ...
}

Isn’t it strange that the function takes the cross product w as an input argument instead of returning it as an output?

To understand the issue, consider a simplified function that returns an array:

%%cpp
auto foo() {
    double w[1]{123};
    cout << *w << " @" << static_cast<void *>(w) << '\n';
    return w;
}
auto w=foo()
input_line_65:4:12: warning: address of stack memory associated with local variable 'w' returned [-Wreturn-stack-address]
    return w;
           ^
123 @0x7fffa7cff7a8
(double *) @0x7fffa7cff470

The return type is double * instead of double[1], showing that an array decays to a pointer when returned by a function.

Note however that there is a warning about the address of stack memory ... returned. To see the value it points to:

%%cpp
cout << *w << " @" << static_cast<void *>(w) << '\n';
w[0] // equal 1?
6.95328e-310 @0x7fffa7cff7a8
(double) 0.0000000
%%hermes
Explain briefly what stack memory is in C++ and why returning address of stack
memory gives a warning?

As a comparison, consider the following function, which returns a locally defined double?

%%cpp
auto foo() {
    double w=123.;
    return w;
}
auto w=foo()
(double) 123.00000

One way to return an array is to declare it as static:

%%cpp
auto foo() {
    static const double w[1]{123};
    return w;
}
auto w=foo();
w[0]
(const double) 123.00000

The memory location w[0] has static storage duration, and therefore remains available throughout the execution of the program. However, the function is not thread-safe unless w is declared to be constant.

How to return a mutable array safely?

%%cpp
auto foo() {
    auto w=new double[1]{123};
    return w;
}
auto w=foo();
++w[0]
(double) 124.00000

The above function returns a mutable array by dynamically allocating memory on the heap memory using the new expression. It is thread-safe because every call to the function creates a distinct heap-allocated array, and so there is no shared state between threads. The heap memory remains accessible after the function call because it is not tied to the function’s stack frame.

Since every function call creates a new array, and the memory is not released after the call completes, the caller is responsible for freeing the memory with delete[].

%%cpp
delete[] w;
// w           // fails as the memory for w is released.
// delete[] w; // fails for the same reason.
w=nullptr;     // prevents accidental reuse or double deletion.
Solution to Exercise 8
  • The array v cannot be declared with size d because d is not a compile-time constant.

  • v should not be returned because its elements are in stack memory that is automatically cleaned up after the function call completes.

  • The array x can only be initialized with an initializer list, not a pointer.

Thanks to object-oriented programming, C++11 defines a value-type container std::array from <array>:

%%cpp
auto foo() {
    auto w=array<double, 1>{123};
    return w;
}
auto w=foo();
++w[0];
w
(std::array<double, 1> &) { 124.00000 }

This works as if w were a simple data type:

  • The array object is correctly returned because it is directly constructed into the caller’s result object via the return value optimization.

  • The function is thread-safe since each call creates a distinct array object.

  • The memory is released automatically when the result object goes out of scope.

We can now modify Program 1 to return the cross product:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * @brief Computes the cross product of two 3D vectors.
 *
 * This function calculates the cross product of two 3-dimensional (3D) vectors `u` and `v`,
 * both represented as `std::array<double, 3>`. The result is a new vector that is
 * perpendicular to both `u` and `v`, following the right-hand rule.
 *
 * @param u The first input vector.
 * @param v The second input vector.
 * @return A 3D vector representing the cross product of `u` and `v`.
 * @example
 * auto w = cross({1,0,0}, {0,0,1}); // (vector3D &) { 0.0, -1.0, 0.0 }
 */
using vector3d = std::array<double, 3>;    // type alias

vector3d cross(const vector3d &u, const vector3d &v) {
    vector3d w;
    w[0] = u[1]*v[2] - u[2]*v[1];
    w[1] = u[2]*v[0] - u[0]*v[2];
    w[2] = u[0]*v[1] - u[1]*v[0];
    return w;
}

Program 5:Compute the cross product u×v\M{u}\times \M{v} using std::array from <array>.

For convenience, the code defines a type alias vector3d for the type std::array<double, 3>:

%%cpp
using vector3d=std::array<double, 3>;

The type alias can then be used to declare the parameters and return value:

%%cpp
vector3d cross(const vector3d &u, const vector3d &v) {
    vector3d w;
    w[0] = u[1]*v[2] - u[2]*v[1];
    w[1] = u[2]*v[0] - u[0]*v[2];
    w[2] = u[0]*v[1] - u[1]*v[0];
    return w;
}

auto w = cross({1,0,0}, {0,0,1});
w
(vector3d &) { 0.0000000, -1.0000000, 0.0000000 }

The array container w is defined within the function and returned to the caller, just like other simple data types:

vector3d cross(..., ...) {
    vector3d w;
    w[0] = ...
    w[1] = ...
    w[2] = ...
    return w;
}

As another example demonstrating the benefit of array container, the following code implements the wheel factorization to check whether a number n is prime or not:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
 * @brief Determines whether a given unsigned long long integer is prime using wheel factorization.
 *
 * This function checks for primality by first testing divisibility against a small set of base primes {2, 3, 5}.
 * It then uses wheel factorization based on the LCM of these primes (30) to efficiently skip non-candidate divisors.
 * Candidate divisors are generated by adding coprime offsets to multiples of 30.
 *
 * @param n The number to check for primality.
 * @return true if n is prime, false otherwise.
 *
 * @details
 * - The wheel is constructed using the coprimes of 30: {1, 7, 11, 13, 17, 19, 23, 29}.
 * - These offsets are added to each multiple of 30 to generate candidate divisors.
 * - The function stops checking once the candidate divisor exceeds sqrt(n).
 *
 * @note This method avoids redundant checks for multiples of 2, 3, and 5, improving efficiency for large numbers.
 *
 * @example
 *     bool result = is_prime(97); // returns true
 */
bool is_prime(const unsigned long long n) {
    if (n < 2) return false;

    // Base primes used for initial divisibility check
    auto basis = to_array<unsigned long long>({2, 3, 5});

    // Offsets that are coprime to 30 (LCM of 2, 3, 5)
    auto offsets = to_array<unsigned long long>({1, 7, 11, 13, 17, 19, 23, 29});

    const unsigned long long m = 30; // LCM of base primes
    const unsigned long long limit = sqrt(n); // No need to check beyond sqrt(n)

    // Check divisibility by base primes
    for (auto p : basis)
        if (p > limit) return true; // No need to check further
        else if (n % p == 0) return false;

    // Use wheel factorization to check remaining candidates
    unsigned long long d;
    for (unsigned long long i = 0uLL;; i += m) {
        for (auto j : offsets) {
            d = i + j;
            if (d > limit) return true; // No divisor found up to sqrt(n)
            else if (n % d == 0 && d > 1) return false; // Found a divisor
        }
    }
}

Program 6:Check whether a non-negative integer is prime.

%%cpp
bool is_prime(const unsigned long long n) {
    if (n<2) return false;
    auto basis=to_array<unsigned long long>({2,3,5});
    auto offsets=to_array<unsigned long long>({1, 7, 11, 13, 17, 19, 23, 29});
    const unsigned long long m=30, limit=sqrt(n);
    for (auto p : basis)
        if (p > limit) return true; else
        if (n % p == 0) return false;
    unsigned long long d;
    for (unsigned long long i=0uLL;; i+=m)
        for (auto j : offsets)
            if ((d=(i+j))>limit) return true; else
            if (n%d==0 && d>1) return false;
}
%%cpp
for (auto n: views::iota(0, 100)) if (is_prime(n)) cout << n << ' ';
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 

Vector Container

Recall that the declaration const auto v[3] in void print_vector3_(const auto v[3]) is the same as const auto v[], which decays to const auto *v. Since the size information is lost, we might as well define a more generate function below to print an arbitrarily long vector:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * @brief Prints the contents of a vector-like array to standard output in formatted style.
 *
 * This function takes an array of elements and its size, then prints the elements
 * in a comma-separated list enclosed in square brackets. The formatting uses `std::format`
 * for consistent output.
 *
 * @tparam T Type of the elements in the array (deduced automatically).
 * @param v Array of elements to print.
 * @param n Number of elements in the array.
 *
 * @note If the array is empty (`n == 0`), nothing is printed.
 * @example
 *     int arr[] = {1, 2, 3};
 *     print_vector(arr, 3); // Output: [1, 2, 3]
 */
void print_vector(const auto v[], const size_t n) {
    if (n) {
        cout << format("[{}", v[0]);
        for (size_t i = 1; i < n; ++i)
            cout << format(", {}", v[i]);
        cout << "]\n";
    }

Program 7:Print the contents of a C-style array.

%%cpp
void print_vector(const auto v[], const size_t n) {
    if (n) {
        cout << format("[{}", v[0]);
        for (size_t i=1; i<n; ++i) cout << format(", {}", v[i]);
        cout << "]\n";
    }
}
print_vector((int[]){1}, 1);
print_vector((int[]){1, 2}, 2);
print_vector((int[]){1, 2, 3}, 3);
print_vector((int[]){1, 2, 3, 4}, 4);
print_vector((int[]){1, 2, 3, 4, 5}, 5);
[1]
[1, 2]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3, 4, 5]

In the function calls, the size is passed as a separate argument n because the size information in v is lost as v decays to a pointer.

To print std::array instead:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * @brief Prints the contents of a fixed-size std::array in vector format.
 *
 * This function outputs the elements of the array in the format: [x1, x2, ..., xN].
 * It uses C++11's std::array from <array> and std::format for formatting.
 *
 * @tparam T Type of the elements in the array.
 * @tparam N Size of the array.
 * @param v The std::array to be printed.
 * @example
 * print_vector(array<int, 4>{1, 2, 3, 4});
 * print_vector(array<int, 5>{1, 2, 3, 4, 5});
 */
template <class T, size_t N>
void print_vector(const array<T, N> &v) {
    if (N>0) {
        auto it=v.begin();
        cout << format("[{}", *it++);
        for (;it != v.end(); it++) cout << format(", {}", *it);
        cout << "]\n";
    }
}

Program 8:Print the contents of an array container.

%%cpp
template <class T, size_t N>
void print_vector(const array<T, N> &v) {
    if (N>0) {
        auto it=v.begin();
        cout << format("[{}", *it++);
        for (;it != v.end(); it++) cout << format(", {}", *it);
        cout << "]\n";
    }
}
print_vector(to_array({1}));
print_vector(to_array({1, 2}));
print_vector(to_array({1, 2, 3}));
print_vector(to_array({1, 2, 3, 4}));
print_vector(to_array({1, 2, 3, 4, 5}));
[1]
[1, 2]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3, 4, 5]

The above code is a template declaration which allows the function print_vector to take std::array<T, N> with any typename or class T and a fixed size N. It also uses iterators to traverse the array.

%%hermes
Explain briefly how to use iterators to traverse an array efficiently in C++.
Solution to Exercise 9

template provides a stronger compile-time guarantee by declaring

  • the argument to be array<T, N>, which guarantees empty is available; and

  • the types of T and N to be class and size_t, which ensures array<T, N> is a valid array type.

Solution to Exercise 10

This is because the size of a statically allocated array must be known at compile time.

A common strategy is to set the size to a large enough constant:

%%cpp
constexpr auto N=5u;
int v[N];
for (auto i=0u; i<N; ++i) v[i]=i+1;
for (auto n=1u; n<=N; ++n) print_vector(v, n);
[1]
[1, 2]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3, 4, 5]

But what if we do not know how large is large enough at compile time? An alternative is to dynamically allocate the memory to the heap:

%%cpp
for (auto n=1u; n<=5; ++n) {
    auto v=new int[5];
    for (auto i=0u; i<n; ++i) v[i]=i+1;
    print_vector(v, n);
    delete[] v;   // 😈: This line can be removed. 👨🏻‍🏫: No.
}
[1]
[1, 2]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3, 4, 5]

To avoid memory leak, C++11 offers variable-length containers such as std::vector from <vector>. This is used in the following implementation of print_vector:

%%cpp
template <class T>
void print_vector(const vector<T> &v) {
    if (!v.empty()) {
        cout << format("[{}", v[0]);
        for (auto vi: v) cout << format(", {}", vi); // range-based for loop
        cout << "]\n";
    }
}

The print_vector function used the method empty to check whether v is an empty vector.

We can now use a for loop to modify the length of a vector:

{
    vector<double> v; /* Creates an empty vector using allocator<double> internally.
                         No memory is allocated yet; capacity is 0. */

    for (auto n = 1u; n <= 5; ++n) { // Loop to insert values 1 to 5
        v.push_back(n); /* Adds 'n' to the vector.
                           If current capacity is full, vector allocates more memory:
                           - Calls allocator.allocate(new_capacity)
                           - Moves existing elements to new memory
                           - Calls allocator.deallocate(old_memory)
                           - Constructs new element with allocator.construct() */
        print_vector(v);
    }
}
/* When 'v' goes out of scope at the end of main:
   - vector's destructor is called
   - Calls allocator.destroy() for each element
   - Calls allocator.deallocate() to free the memory block */
%%cpp
{
    vector<double> v;
    for (auto n=1u; n<=5; ++n) {
        v.push_back(n);
        print_vector(v);
    }
}
[1, 1]
[1, 1, 2]
[1, 1, 2, 3]
[1, 1, 2, 3, 4]
[1, 1, 2, 3, 4, 5]
%%hermes
Explain briefly what allocators are in C++ and the benefits of using allocators
instead of new and delete in C++?

The code used the method push_back to append an element to the end of the vector.

The dot product in Program 2 can also be reimplemented to use the vector container:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using vectord = std::vector<double>;

/**
 * @brief Computes the dot product of two vectors of type vectord.
 *
 * This function calculates the dot product of two vectors `u` and `v`.
 * It throws a runtime error if the vectors are not of the same length.
 *
 * @param u A vector of doubles.
 * @param v A vector of doubles.
 * @return The dot product of vectors `u` and `v` as a double.
 *
 * @throws std::runtime_error if the input vectors have different lengths.
 *
 * @example
 * dot({1,1,1,1}, {1,1,1,1}); // 4.0
 */
auto dot(const vectord &u, const vectord &v) {
    if (u.size() != v.size()) 
        throw std::runtime_error("u and v cannot have different lengths.");
    auto prod = 0.;
    for (size_t i = 0, n = u.size(); i < n; ++i) 
        prod += u[i] * v[i];
    return prod;
}

Program 9:Compute the dot product u⋅v\M{u}\cdot \M{v} using std::vector from <vector>.

%%cpp
using vectord=std::vector<double>;
%%cpp
auto dot(const vectord &u, const vectord &v) {
    if (u.size()!=v.size()) 
        throw runtime_error("u and v cannot have different lengths.");
    auto prod=0.;
    for (size_t i=0, n=u.size(); i<n; ++i) prod += u[i]*v[i];
    return prod;
}
// dot({1,1,1}, {1,1,1,1});  // fails: size mismatch
dot({1,1,1,1}, {1,1,1,1})
(double) 4.0000000

The code ensures the input values u and v are valid by checking whether they have the same size using the method size.

To repeatedly calculate the norms of the vectors of all ones for different dimensions:

%%cpp
auto norm(const vectord &v) {
    return sqrt(dot(v, v));
}
vectord v;
for (auto n=0u; n<10; ++n) {
    cout << norm(v) << ' ';
    v.push_back(1);
}
0 1 1.41421 1.73205 2 2.23607 2.44949 2.64575 2.82843 3 

To repeatedly calculate the dot products of orthogonal vectors for different dimensions:

%%cpp
vectord u{0}, v{0};
for (auto n=0u; n<10; ++n) {
    u.insert(u.begin(),1);
    v.push_back(1);
    cout << dot(u, v) << ' ';
    u[0]=v[v.size()-1]=0;
}
0 0 0 0 0 0 0 0 0 0 

The code used the method insert to insert 1 to the beginning position specified by the method begin.

%%cpp
auto zeros(const int d) {
    /*
    ### BEGIN SOLUTION
    */
    return vector<double>(d);
    /*
    ### END SOLUTION
    */
}
// test
zeros(11)
(std::vector<double>) { 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000 }
%%cpp
/**
 * @brief Generates all prime numbers less than or equal to a given number.
 *
 * This function uses the Sieve of Eratosthenes algorithm to find all prime numbers
 * up to and including the input value `n`. It returns a std::vector of the primes.
 *
 * @param n The upper bound (inclusive) for prime number generation.
 * @return std::vector<unsigned long long> A list of prime numbers ≤ n.
 */
std::vector<unsigned long long> prime_below(unsigned long long n) {
    std::vector<unsigned long long> prime_numbers;
    std::vector<bool> is_prime(n+1, true);
    /*
    ### BEGIN SOLUTION
    */
    for (auto i = 2uLL; i <= n; ++i)
        if (is_prime[i]) {
            prime_numbers.push_back(i);
            if (i <= n/i)
                for (auto j = i * i; j <= n; j += i)
                    is_prime[j] = false;
        }
    return prime_numbers;
    /*
    ### END SOLUTION
    */
}
prime_below(100)
(std::vector<unsigned long long>) { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97 }
%%cpp
if (prime_below(100000uLL).back() != 99991uLL) throw runtime_error("test failed");
Footnotes
  1. In comparison, Fortran uses a column-major ordering or F-ordering, where the index of a later dimension (known as a column index) incremented slower.