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.
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:
#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);
} 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.
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".
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:
#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));
} 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 + jandp - jare defined only while the result stays between the first element and one past the last element of the same array. Formingmarks - 1ormarks + 6is undefined behaviour, even if it is never dereferenced, so a reverse loop that runsfor (int* q = marks + 4; q >= marks; --q)ends by formingmarks - 1and is wrong.- Dereferencing the one-past-the-end pointer is undefined behaviour: it is a position, not an element.
p - qis 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 astd::map,std::lessprovides 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:
#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);
} 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):
#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>));
} 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:
#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);
} 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:
#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
} 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 intototal, 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
-O2build read 221 silently, and-D_GLIBCXX_ASSERTIONSturned 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_ASSERTIONSturns 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:
#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());
}
} 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:
// 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);
} 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.
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 + imovesielements,sizeof(T)bytes each;p[i]means*(p + i);q - pcounts 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
sizeofin the function measures the pointer: the warning-Wsizeof-array-argumentcatches 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 addsat().- AddressSanitizer misses an overflow that stays inside one object, which the library’s own checks still catch.
References
- Working Draft, Programming Languages: C++ (N4950), [expr.add], [expr.rel], [dcl.fct], [views.span], [comparisons.general] (ISO/IEC JTC1/SC22/WG21)
- P0122R7: span: bounds-safe views for sequences of objects (ISO/IEC JTC1/SC22/WG21)
- P2821R5: span.at() (ISO/IEC JTC1/SC22/WG21)
- Hardening Modes (libc++ documentation) (LLVM Project)
- Macros (The GNU C++ Library manual) (GNU Project)
- GCC 15 Release Series, Changes, New Features, and Fixes (GNU Project)
- AddressSanitizer (Clang documentation) (LLVM Project)
- C++ Core Guidelines (I.13, F.24, R.14, Bounds.1) (Standard C++ Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress