Slices - Pointers to arrays

A fundamental problem with pointers that we stumbled upon in the previous section was that a pointer does not contain enough information to fully describe the memory location it points to. In particular, given a single pointer we have no way of knowing whether this pointer refers to just one instance of a type, or an array:

int main() {
    int* ptr = get_ptr();
    // If ptr is not null, this should at least be valid:
    std::cout << *ptr << std::endl;
    // But is this also valid?
    std::cout << ptr[1] << std::endl;

    return 0;
}

Just from looking at the pointer, no chance to figure it out. So we have to resort to the method documentation of get_ptr(). Which is fine, but not really what we as programmers would want. It would be better if we could write code that makes it obvious how to use it, without looking into the documentation.

We could establish a convention, something like 'Always use references for single instances, and pointers for arrays'. But any convention is fragile and prone to error, so that doesn't sound like a good idea. Instead, why not use a dedicated type for dynamic arrays? We can wrap our pointer inside a type that behaves like an array. What's the defining property of an array? It is a contiguous sequence of memory. To model this with a type, we can either use two pointers - one for the beginning of the array and one for the end - or we use a pointer and a size. The second approach might seem more intuitive, so let's go with that:

#include <iostream>

template<typename T>
struct DynamicArray {
    T* start;
    size_t size;
};

DynamicArray<int> get_ptr() {
    int* arr = new int[42];
    return {arr, 42};
}

int main() {
    DynamicArray<int> arr = get_ptr();
    for(size_t idx = 0; idx < arr.size; ++idx) {
        std::cout << arr.start[idx] << std::endl;
    }

    return 0;
}

Run this example

This DynamicArray type is missing a lot of syntactic sugar to make it really usable, but this is the main idea: A wrapper around a pointer and a size. In this toy example, it would have been better if we had used std::vector, because the get_ptr() method actually returns an owning pointer to the memory, but we can think of many scenarios where we don't want to own an array and instead only want a view on the array. This is like a reference, but for arrays.

Image showing memory structure of std::span

Of course, such types exist already in C++ and Rust. C++ took quite some time to standardize something like that, with C++20 the std::span type has been standardized, as an alternative gsl::span from the Guidelines Support Library can be used. Rust goes a step further and has built-in support for views through slices. Under the hood, slices are also just a pointer and a size, but they are integrated deeply into the Rust language:

fn main() {
    let numbers: [u32; 4] = [1, 2, 3, 4];

    let numbers_slice: &[u32] = &numbers[..]; //[..] syntax takes a slice of the whole array

    //Slices behave like arrays: They have a len() method and support the [] operator:
    println!("{}", numbers_slice.len());
    println!("{}", numbers_slice[0]);

    //We can even loop over slices (we will see in a later chapter how this works under the hood):
    for number in numbers_slice {
        println!("{}", number);
    }
}

Run this example

We haven't really talked about strings a lot, but strings are a special type of dynamic array storing charactersOr UTF-8 code points, in the case of Rust strings.. Since strings play a very important role in almost every program, strings typically get some extra types and functions that set them apart from plain arrays or vectors. C++ has a special variant of std::span for strings, called std::basic_string_view, which is already available in C++17. Rust has a special slice type for string slices, called str.