Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 6 – References, pointers and memory

Pointer arithmetic, arrays and std::span

How p + 1 moves through an array, which pointer arithmetic is undefined, and how std::span passes any contiguous data safely, with hardened bounds checks run.

  • Intermediate
  • 25 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Move through an array with pointer arithmetic and measure distances with pointer subtraction
  • Recognise pointer arithmetic and indexing that are undefined behaviour
  • Pass contiguous data with one std::span parameter and slice it with first, last and subspan
  • Configure the bounds checks of libc++ and libstdc++ and read what they report

Before you start

What you’ll learn · Before you start

What you will learn

  • Move through an array with pointer arithmetic and measure distances with pointer subtraction
  • Recognise pointer arithmetic and indexing that are undefined behaviour
  • Pass contiguous data with one std::span parameter and slice it with first, last and subspan
  • Configure the bounds checks of libc++ and libstdc++ and read what they report

Before you start

On this page

Adding an integer to a pointer moves it by whole elements, not bytes: if p points to element 2 of an int array, p + 1 points to element 3, which is sizeof(int) bytes further on, and p[i] is just another way to write *(p + i). That is pointer arithmetic, and it is only defined inside one array: a pointer may move from the first element to one position past the last, and no further. Everything outside that range is undefined behaviour, even before anything is read.

A raw pointer does not know how many elements follow it, which is why C-style code passes a pointer and a length side by side and why so many memory bugs are off by one. C++20’s std::span bundles the two into one small object: a view of a contiguous sequence that it does not own, built from a C array, a std::array or a std::vector, and cut into pieces without copying. This lesson walks an array with pointers, shows which arithmetic is undefined, then replaces the pointer-and-length pair with std::span, including the checks that the standard libraries can add to span’s indexing.

Moving through an array

In most expressions an array converts to a pointer to its first element, so int* p = marks; needs no &. From there, p + i points i elements further, and *(p + i) and p[i] are the same element:

walk.cpp C++ · walk.cpp
#include <cstddef>
#include <cstdint>
#include <print>

// The address of a pointer as a number, to measure distances in bytes.
std::uintptr_t address(const void* p) { return reinterpret_cast<std::uintptr_t>(p); }

int main() {
    int marks[5] = {72, 85, 64, 91, 80};
    int* p = marks;                              // the array converts to a pointer to its first element
    std::println("*p = {}, *(p + 2) = {}, p[2] = {}", *p, *(p + 2), p[2]);
    std::println("p + 1 is {} bytes after p", address(p + 1) - address(p));

    double prices[3] = {9.5, 12.0, 7.25};
    double* d = prices;
    std::println("d + 1 is {} bytes after d", address(d + 1) - address(d));

    int sum = 0;
    for (const int* it = marks; it != marks + 5; ++it)   // marks + 5 points one past the last element
        sum += *it;
    std::ptrdiff_t count = (marks + 5) - marks;
    std::println("sum {} over {} elements", sum, count);
}
Steps of 4 bytes for int, 8 for double Python · walk_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra walk.cpp -o walk && ./walk
g++-16 -std=c++23 -Wall -Wextra walk.cpp -o walk && ./walk
""")

Output

$ clang++ -std=c++23 -Wall -Wextra walk.cpp -o walk && ./walk
*p = 72, *(p + 2) = 64, p[2] = 64
p + 1 is 4 bytes after p
d + 1 is 8 bytes after d
sum 392 over 5 elements
$ g++-16 -std=c++23 -Wall -Wextra walk.cpp -o walk && ./walk
*p = 72, *(p + 2) = 64, p[2] = 64
p + 1 is 4 bytes after p
d + 1 is 8 bytes after d
sum 392 over 5 elements

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 walk_session.py

The byte distances show the rule: p + 1 is 4 bytes after p because an int is 4 bytes here, and d + 1 is 8 bytes after d because a double is 8. You never multiply by the size yourself; the type of the pointer does it.

The loop uses the idiom behind the standard algorithms, a half-open range: start at the first element and stop when the pointer reaches marks + 5, one position past the last element. That pointer may be formed and compared, but never dereferenced. Subtracting two pointers into the same array gives the number of elements between them, a std::ptrdiff_t: here (marks + 5) - marks is 5.

The array marks of five ints, with p at element 0, p + 2 at element 2 and marks + 5 one past the end; a span holds its address and size 5.int marks[5], 4 bytes per elementstd::span<const int> s = marks;s.data() is the address of marks[0]s.size() is 572[0]p85[1]64[2]p + 291[3]80[4]one pastthe endmarks + 5s views all five elements

Pointers into an array, and the span that describes it

Text description of the diagram

The diagram shows the array int marks[5] as a row of five boxes holding 72, 85, 64, 91 and 80, at indexes 0 to 4, each 4 bytes long. A sixth box with a dashed outline after them is labelled "one past the end": it is not an element.

Three positions are named inside the boxes: p, which is marks itself, at element 0; p + 2 at element 2, which holds 64; and marks + 5 at the dashed box one past the end. A pointer one past the end may be formed and compared, but not dereferenced.

Above the row is a box for std::span<const int> s = marks;. Its data() is the address of marks[0], and its size() is 5. An arrow from it down to the row is labelled "s views all five elements".

Programmer Calculator (Bitwise) Print two element addresses with static_cast<const void*>, then subtract them in hex to check the 4-byte step.

Arrays passed to functions become pointers

A function parameter declared as an array is really a pointer: [dcl.fct] adjusts a parameter of type “array of T” to “pointer to T”. The size is lost at the call, and sizeof inside the function measures the pointer:

decay.cpp C++ · decay.cpp
#include <cstddef>
#include <print>

// Looks like it takes an array; it takes a pointer.
std::size_t count(const int marks[]) {
    return sizeof(marks) / sizeof(marks[0]);
}

int main() {
    int marks[5] = {72, 85, 64, 91, 80};
    std::println("in main:  {} elements", sizeof(marks) / sizeof(marks[0]));
    std::println("in count: {} elements", count(marks));
}
sizeof in main and in a function Python · decay_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra decay.cpp -o decay && ./decay
g++-16 -std=c++23 -Wall -Wextra decay.cpp -o decay && ./decay
""")

Output

$ clang++ -std=c++23 -Wall -Wextra decay.cpp -o decay && ./decay
decay.cpp:6:18: warning: sizeof on array function parameter will return size of 'const int *' instead of 'const int[]' [-Wsizeof-array-argument]
    6 |     return sizeof(marks) / sizeof(marks[0]);
      |                  ^
decay.cpp:5:29: note: declared here
    5 | std::size_t count(const int marks[]) {
      |                             ^
decay.cpp:6:26: warning: 'sizeof (marks)' will return the size of the pointer, not the array itself [-Wsizeof-pointer-div]
    6 |     return sizeof(marks) / sizeof(marks[0]);
      |            ~~~~~~~~~~~~~ ^
decay.cpp:5:29: note: pointer 'marks' declared here
    5 | std::size_t count(const int marks[]) {
      |                             ^
2 warnings generated.
in main:  5 elements
in count: 2 elements
$ g++-16 -std=c++23 -Wall -Wextra decay.cpp -o decay && ./decay
decay.cpp: In function 'std::size_t count(const int*)':
decay.cpp:6:19: warning: 'sizeof' on array function parameter 'marks' will return size of 'const int*' [-Wsizeof-array-argument]
    6 |     return sizeof(marks) / sizeof(marks[0]);
      |                  ~^~~~~~
decay.cpp:5:29: note: declared here
    5 | std::size_t count(const int marks[]) {
      |                   ~~~~~~~~~~^~~~~~~
in main:  5 elements
in count: 2 elements

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 decay_session.py

In main, sizeof(marks) is the whole array, 20 bytes, so the count is 5. In count, it is the size of a pointer, 8 bytes, divided by 4, so the count is 2: wrong, and no error. Both compilers warn with -Wsizeof-array-argument, and Clang adds -Wsizeof-pointer-div. This is the reason the C++ Core Guidelines say, in I.13, do not pass an array as a single pointer.

The rules that make pointer arithmetic undefined

The standard’s section on additive operators, [expr.add], allows exactly this much:

  • p + j and p - j are defined only while the result stays between the first element and one past the last element of the same array. Forming marks - 1 or marks + 6 is undefined behaviour, even if it is never dereferenced, so a reverse loop that runs for (int* q = marks + 4; q >= marks; --q) ends by forming marks - 1 and is wrong.
  • Dereferencing the one-past-the-end pointer is undefined behaviour: it is a position, not an element.
  • p - q is defined only when both point into the same array (or one past its end).
  • Comparing pointers into different arrays with < gives an unspecified result under [expr.rel]: either answer is allowed, and nothing makes it consistent. When you need an order of unrelated pointers, for example as keys of a std::map, std::less provides a consistent total order ([comparisons.general]), and the map uses it by default.

Off-by-one errors are the usual way these rules are broken. Here <= instead of != makes the loop read one element too many:

off_by_one.cpp C++ · off_by_one.cpp
#include <print>

int main() {
    int marks[5] = {72, 85, 64, 91, 80};
    int sum = 0;
    for (const int* it = marks; it <= marks + 5; ++it)   // <= reads one element past the end
        sum += *it;
    std::println("sum {}", sum);
}
One element past the end, caught by AddressSanitizer Python · off_by_one_session.py
from session import run

run("""
clang++ -std=c++23 -g -fsanitize=address off_by_one.cpp -o off_by_one && ./off_by_one
g++-16 -std=c++23 -g -fsanitize=address off_by_one.cpp -o off_by_one && ./off_by_one
""")

Output

$ clang++ -std=c++23 -g -fsanitize=address off_by_one.cpp -o off_by_one && ./off_by_one
==…==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x…
READ of size 4 at 0x… thread T0
    #0 in main off_by_one.cpp:7
Address 0x… is located in stack of thread T0 at offset 52 in frame
    #0 in main off_by_one.cpp:3
    [32, 52) 'marks' (line 4) <== Memory access at offset 52 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow off_by_one.cpp:7 in main
[40 more lines of the report left out]
/bin/sh: line 1: … Abort trap: 6           ./off_by_one
[exit status 134]
$ g++-16 -std=c++23 -g -fsanitize=address off_by_one.cpp -o off_by_one && ./off_by_one
==…==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x…
READ of size 4 at 0x… thread T0
    #0 in main off_by_one.cpp:7
Address 0x… is located in stack of thread T0 at offset 116 in frame
    #0 in main off_by_one.cpp:3
    [96, 116) 'marks' (line 4) <== Memory access at offset 116 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow off_by_one.cpp:7 in main
[41 more lines of the report left out]
/bin/sh: line 1: … Abort trap: 6           ./off_by_one
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 off_by_one_session.py

Both sanitizer builds stop at line 7 with stack-buffer-overflow. The report says the read at offset 52 overflows marks, which occupies offsets 32 to 52 in Clang’s frame: the 20 bytes of the array, and the read was the first byte after them.

std::span: one parameter for any contiguous data

std::span<T> from <span> holds a pointer to the first element and the number of elements. It owns nothing: in the words of the standard, a span is a view over a contiguous sequence of objects whose storage is owned by some other object. A parameter of type std::span<const int> accepts a C array, a std::array and a std::vector of int without copying them, and a span cuts into smaller spans with first(n), last(n) and subspan(offset, count):

span_total.cpp C++ · span_total.cpp
#include <array>
#include <print>
#include <span>
#include <vector>

// One parameter accepts any contiguous ints: a C array, a std::array, a std::vector or part of one.
int total(std::span<const int> marks) {
    int sum = 0;
    for (int m : marks) sum += m;
    return sum;
}

int main() {
    int c_array[] = {72, 85, 64};
    std::array<int, 4> fixed{91, 80, 77, 68};
    std::vector<int> growing{55, 60};
    std::println("totals: {}, {}, {}", total(c_array), total(fixed), total(growing));

    std::span<const int> all = fixed;
    std::println("first(2) {}, last(1) {}, subspan(1, 2) {}", total(all.first(2)), total(all.last(1)), total(all.subspan(1, 2)));

    std::println("sizeof(std::span<const int>) = {}", sizeof(std::span<const int>));
    std::println("sizeof(std::span<const int, 4>) = {}", sizeof(std::span<const int, 4>));
}
Three kinds of array, one function Python · span_total_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra span_total.cpp -o span_total && ./span_total
g++-16 -std=c++23 -Wall -Wextra span_total.cpp -o span_total && ./span_total
""")

Output

$ clang++ -std=c++23 -Wall -Wextra span_total.cpp -o span_total && ./span_total
totals: 221, 316, 115
first(2) 171, last(1) 68, subspan(1, 2) 157
sizeof(std::span<const int>) = 16
sizeof(std::span<const int, 4>) = 8
$ g++-16 -std=c++23 -Wall -Wextra span_total.cpp -o span_total && ./span_total
totals: 221, 316, 115
first(2) 171, last(1) 68, subspan(1, 2) 157
sizeof(std::span<const int>) = 16
sizeof(std::span<const int, 4>) = 8

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 span_total_session.py

total is written once and works for all three. first(2) is 91 and 80, last(1) is 68, and subspan(1, 2) is 80 and 77. The size lines show what a span is made of on both libraries: 16 bytes, a pointer and a size, for the usual std::span<const int>. A span with a size fixed in its type, std::span<const int, 4>, needs no size member and takes 8 bytes. Use std::span<const T> for data the function only reads and std::span<T> when it writes elements.

The C++ Core Guidelines ask for this in three places: F.24, use a span<T> to designate a half-open sequence; R.14, avoid [] parameters and prefer span; and the bounds profile’s Bounds.1, don’t use pointer arithmetic, use span instead. When you call a C function that gives you a pointer and a length, wrap them once, std::span<const int> s(ptr, n);, and pass the span from there on.

A span can dangle like a pointer: it refers to storage owned elsewhere, so a span of a vector must not outlive the vector, and pushing elements into the vector may move its storage and leave the span pointing at freed memory.

Sliding windows with subspan

subspan makes windows over data cheap, because each window is a new pointer and size, not a copy. Here are the totals of every three consecutive days of sales, and the best window:

windows.cpp C++ · windows.cpp
#include <cstddef>
#include <print>
#include <span>
#include <vector>

int total(std::span<const int> days) {
    int sum = 0;
    for (int d : days) sum += d;
    return sum;
}

int main() {
    const std::vector<int> sales{12, 7, 15, 9, 20, 4, 11};
    const std::span<const int> all = sales;
    const std::size_t width = 3;

    std::size_t best = 0;
    for (std::size_t start = 0; start + width <= all.size(); ++start) {
        std::span<const int> window = all.subspan(start, width);     // no copy: a view of 3 elements
        std::println("days {}-{}: {}", start + 1, start + width, total(window));
        if (total(window) > total(all.subspan(best, width))) best = start;
    }
    std::println("best {} days start on day {}", width, best + 1);
}
Every 3-day window Python · windows_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra windows.cpp -o windows && ./windows
""")

Output

$ clang++ -std=c++23 -Wall -Wextra windows.cpp -o windows && ./windows
days 1-3: 34
days 2-4: 31
days 3-5: 44
days 4-6: 33
days 5-7: 35
best 3 days start on day 3

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 windows_session.py

The loop condition start + width <= all.size() keeps every window inside the data. Days 3 to 5 sum to 44, the best of the five windows.

Bounds checks: what span does and does not check

span’s operator[] does not check the index: its precondition is idx < size(), and breaking a precondition is undefined behaviour. This program reads one element past the end of a span over an array that is followed by another member in the same struct:

span_bounds.cpp C++ · span_bounds.cpp
#include <print>
#include <span>

struct Row {
    int marks[3];
    int total;
};

int main() {
    Row row{{72, 85, 64}, 221};
    std::span<const int> marks = row.marks;
    std::println("{} marks", marks.size());
    std::println("marks[3] is {}", marks[3]);   // past the end: undefined behaviour
}
marks[3], with and without the library's checks Python · span_bounds_session.py
from session import run

# One index past the end, with and without the standard libraries' checks.
run("""
clang++ -std=c++23 span_bounds.cpp -o span_bounds && ./span_bounds
clang++ -std=c++23 -g -fsanitize=address span_bounds.cpp -o span_bounds && ./span_bounds
clang++ -std=c++23 -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST span_bounds.cpp -o span_bounds && ./span_bounds
clang++ -std=c++23 -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG span_bounds.cpp -o span_bounds && ./span_bounds
g++-16 -std=c++23 -O2 span_bounds.cpp -o span_bounds && ./span_bounds
g++-16 -std=c++23 -O2 -D_GLIBCXX_ASSERTIONS span_bounds.cpp -o span_bounds && ./span_bounds
g++-16 -std=c++23 span_bounds.cpp -o span_bounds && ./span_bounds
""")

Output

$ clang++ -std=c++23 span_bounds.cpp -o span_bounds && ./span_bounds
3 marks
marks[3] is 221
$ clang++ -std=c++23 -g -fsanitize=address span_bounds.cpp -o span_bounds && ./span_bounds
3 marks
marks[3] is 221
$ clang++ -std=c++23 -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST span_bounds.cpp -o span_bounds && ./span_bounds
3 marks
/bin/sh: line 1: … Trace/BPT trap: 5       ./span_bounds
[exit status 133]
$ clang++ -std=c++23 -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG span_bounds.cpp -o span_bounds && ./span_bounds
3 marks
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/span:537: libc++ Hardening assertion __idx < size() failed: span<T>::operator[](index): index out of range
/bin/sh: line 1: … Abort trap: 6           ./span_bounds
[exit status 134]
$ g++-16 -std=c++23 -O2 span_bounds.cpp -o span_bounds && ./span_bounds
3 marks
marks[3] is 221
$ g++-16 -std=c++23 -O2 -D_GLIBCXX_ASSERTIONS span_bounds.cpp -o span_bounds && ./span_bounds
3 marks
/opt/homebrew/Cellar/gcc/16.2.0/include/c++/16/span:288: constexpr std::span<_Type, _Extent>::element_type& std::span<_Type, _Extent>::operator[](size_type) const [with _Type = const int; long unsigned int _Extent = 18446744073709551615; reference = const int&; size_type = long unsigned int]: Assertion '__idx < size()' failed.
/bin/sh: line 1: … Abort trap: 6           ./span_bounds
[exit status 134]
$ g++-16 -std=c++23 span_bounds.cpp -o span_bounds && ./span_bounds
3 marks
/opt/homebrew/Cellar/gcc/16.2.0/include/c++/16/span:288: constexpr std::span<_Type, _Extent>::element_type& std::span<_Type, _Extent>::operator[](size_type) const [with _Type = const int; long unsigned int _Extent = 18446744073709551615; reference = const int&; size_type = long unsigned int]: Assertion '__idx < size()' failed.
/bin/sh: line 1: … Abort trap: 6           ./span_bounds
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 span_bounds_session.py

Read the results in order:

  • The plain Clang build printed marks[3] is 221. The read went past the array into total, the member that happens to come next, and nothing noticed. A wrong value with no crash is the worst outcome.
  • AddressSanitizer printed the same: the read never left the object row, so there is no poisoned memory to touch.
  • libc++’s fast hardening mode, -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST, stopped the program at the bad index with a trap instruction, exit status 133 (128 plus signal 5) and no message. The libc++ documentation recommends this mode for most projects: its checks have little overhead and are meant for production builds.
  • The debug mode adds a message that names the check, __idx < size(), and the function, span<T>::operator[]. It is meant for development, not production.
  • With GCC, the -O2 build read 221 silently, and -D_GLIBCXX_ASSERTIONS turned on libstdc++’s checks. The last build, compiled without -O, stopped too, with no option at all: since GCC 15, libstdc++ enables these assertions by default when optimisation is off, and -D_GLIBCXX_NO_ASSERTIONS turns them off again.

C++26 adds a checked accessor to span, at(), which throws std::out_of_range for an index outside the span, as std::vector::at has always done. Both compilers have it already in their C++26 modes:

span_at.cpp C++ · span_at.cpp
#include <print>
#include <span>
#include <stdexcept>

int main() {
    int marks[] = {72, 85, 64};
    std::span<const int> s = marks;
    try {
        std::println("s.at(3) is {}", s.at(3));
    } catch (const std::out_of_range& e) {
        std::println("out_of_range: {}", e.what());
    }
}
span::at in C++26 mode Python · span_at_session.py
from session import run

# C++26 adds at(), which checks the index and throws std::out_of_range.
run("""
clang++ -std=c++2c -Wall -Wextra span_at.cpp -o span_at && ./span_at
g++-16 -std=c++26 -Wall -Wextra span_at.cpp -o span_at && ./span_at
""")

Output

$ clang++ -std=c++2c -Wall -Wextra span_at.cpp -o span_at && ./span_at
out_of_range: span
$ g++-16 -std=c++26 -Wall -Wextra span_at.cpp -o span_at && ./span_at
out_of_range: span::at(3) out-of-range for span of size 3

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 span_at_session.py

The messages differ, span from libc++ and a full sentence from libstdc++, but the type of the exception is the standard one. In code that must build as C++23, turn on a hardening mode in debug and test builds, and keep explicit checks such as if (i < s.size()) where an index comes from outside the program.

Your turn: a moving average

Write moving_average, which takes the values as a std::span<const double> and returns the average of every run of window consecutive values, in order. When window is 0 or larger than the number of values, it returns an empty vector. The program checks seven cases, including a C array and part of a vector passed through subspan:

moving_average_exercise.cpp C++ · moving_average_exercise.cpp
// Write moving_average: the average of every run of `window` consecutive values, in order. When window is 0 or
// larger than the number of values, return an empty vector.
#include <cstddef>
#include <print>
#include <span>
#include <vector>

std::vector<double> moving_average(std::span<const double> values, std::size_t window) {
    (void)values;                    // replace these three lines
    (void)window;
    return {};
}

int passed = 0;
int checks = 0;

void check(bool ok, const char* what) {
    ++checks;
    if (ok) ++passed;
    std::println("{} {}", ok ? "pass" : "FAIL", what);
}

int main() {
    const std::vector<double> temps{1, 2, 3, 4, 5};
    const double c_array[] = {10, 20, 30, 40};

    check(moving_average(temps, 2) == std::vector<double>{1.5, 2.5, 3.5, 4.5}, "window 2 over 1..5");
    check(moving_average(temps, 5) == std::vector<double>{3}, "one window that covers everything");
    check(moving_average(temps, 1) == temps, "window 1 returns the values themselves");
    check(moving_average(c_array, 3) == std::vector<double>{20, 30}, "a C array, window 3");
    check(moving_average(std::span(temps).subspan(1, 3), 3) == std::vector<double>{3}, "part of a vector, through subspan");
    check(moving_average(temps, 0).empty(), "window 0 gives an empty result");
    check(moving_average(temps, 6).empty(), "a window larger than the data gives an empty result");
    std::println("{} of {} checks passed", passed, checks);
}
The exercise as given Python · moving_average_session.py
from session import run

# The exercise as given: the starting code always returns an empty vector.
run("""
clang++ -std=c++23 -Wall -Wextra moving_average_exercise.cpp -o moving_average_exercise && ./moving_average_exercise
""")

Output

$ clang++ -std=c++23 -Wall -Wextra moving_average_exercise.cpp -o moving_average_exercise && ./moving_average_exercise
FAIL window 2 over 1..5
FAIL one window that covers everything
FAIL window 1 returns the values themselves
FAIL a C array, window 3
FAIL part of a vector, through subspan
pass window 0 gives an empty result
pass a window larger than the data gives an empty result
2 of 7 checks passed

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 moving_average_session.py

As given, the function always returns an empty vector, so only the two “empty result” checks pass. A correct version prints 7 of 7 checks passed with both compilers, and also under -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG, which shows that no span or vector index went out of range.

First hint: return early for the two empty cases, then loop over every start position for which start + window <= values.size() and average values.subspan(start, window). Once that works, try keeping a running sum instead, adding the new value and subtracting the one that left the window, so each step costs the same however wide the window is.

Self-check

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 double* d holds the address 0x1000. How many bytes after 0x1000 is d + 3, when a double is 8 bytes?

    Type a number.

    Show the answer to question 1

    Answer: 24 bytes

    Pointer arithmetic counts elements: d + 3 is three doubles further, 3 × 8 = 24 bytes, so the address is 0x1018.

  2. Question 2 of 5 For int a[5];, which expressions are defined?

    Choose every answer that is right.

    Show the answer to question 2

    Answer:

    • (a + 5) - a
    • a + 5

    A pointer may point from the first element to one past the last, so a + 5 can be formed and compared, and the difference of two such pointers is defined (5). Dereferencing a + 5, or forming a - 1 or a + 6, is undefined behaviour.

  3. Question 3 of 5 In std::size_t count(const int marks[]) { return sizeof(marks); }, what does sizeof(marks) measure on a 64-bit platform?

    Choose one answer.

    Show the answer to question 3

    Answer: A pointer, 8 bytes

    A parameter declared as an array is adjusted to a pointer, so sizeof measures the pointer. Both compilers warn with -Wsizeof-array-argument.

  4. Question 4 of 5 Which of these can be passed to a parameter of type std::span<const int> without writing any conversion?

    Choose one answer.

    Show the answer to question 4

    Answer: An int C array, a std::array<int, 4> and a std::vector<int>

    A span views contiguous storage, which C arrays, std::array and std::vector all are. A std::list stores its elements in separate nodes, so no span can view it.

  5. Question 5 of 5 A program reads s[3] from a span of size 3. Which build stopped it in this lesson's runs?

    Choose one answer.

    Show the answer to question 5

    Answer: Clang with libc++'s fast hardening mode (_LIBCPP_HARDENING_MODE_FAST)

    The fast hardening mode checks the index and traps (exit status 133). AddressSanitizer missed it because the read stayed inside the struct, and the plain and -O2 builds read the neighbouring member silently.

Interview questions

Fresher · What is std::span, and why is it better than passing a pointer and a length?

std::span<T>, added in C++20, is a view of a contiguous sequence of T that it does not own: in practice a pointer to the first element and the number of elements, 16 bytes on a 64-bit platform. One parameter of type std::span<const T> accepts a C array, a std::array, a std::vector or a slice of any of them, without copying, so a caller that passes a container can no longer get the length wrong, and first, last and subspan cut it safely. It is no cure-all: operator[] is unchecked unless a hardened standard library checks it, C++26 adds a checking at(), and a span dangles when the data it views is destroyed or reallocated.

Key takeaways

  • p + i moves i elements, sizeof(T) bytes each; p[i] means *(p + i); q - p counts elements between two pointers into one array.
  • Pointer arithmetic is defined only from the first element to one past the last; forming a pointer outside that range is undefined behaviour even without a read.
  • An array parameter is a pointer, so sizeof in the function measures the pointer: the warning -Wsizeof-array-argument catches it.
  • std::span<const T> replaces a pointer and a length: it accepts any contiguous sequence, slices without copying and takes 16 bytes.
  • span::operator[] is unchecked; libc++’s fast hardening mode traps a bad index, libstdc++ checks with _GLIBCXX_ASSERTIONS (on by default without optimisation since GCC 15), and C++26 adds at().
  • AddressSanitizer misses an overflow that stays inside one object, which the library’s own checks still catch.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.