I have spent the last few weeks building a small mini-STL in C++. This is a learning project: the point is not a library that competes with std, but understanding the design decisions behind containers, views and algorithms by building them myself. I am a big fan of the method-chaining style: my_array.sort().find(5). That raised an interesting question: where do the algorithms live? In the C++ STL they are offered as free functions over pairs of iterators. That lets you implement an algorithm once and apply it to any range. Rust takes a different route with slices. My question was: how does the slice concept carry over to C++?

The answer sits in a 76-line class:

template <typename T>
class Slice {
  public:
    constexpr Slice() noexcept = default;
    constexpr Slice(value_type* data, size_type len) noexcept
        : data_(data), len_(len) {}

    [[nodiscard]] constexpr size_type len() const noexcept { return len_; }
    constexpr value_type* begin() noexcept { return data_; }
    constexpr value_type* end() noexcept { return data_ + len_; }
    constexpr value_type& operator[](size_type index) noexcept { return data_[index]; }

    [[nodiscard]] constexpr SortedSlice<T> sort() {
        // Insertion sort, in place ...
        return SortedSlice<T>(data_, len_);
    }

    constexpr Option<size_type> find(const value_type& value) const noexcept { ... }
    constexpr bool contains(const value_type& value) const noexcept { ... }
};

A Slice<T> owns nothing. It is nothing more than a pair of pointer and length. But this is where all the algorithms live. Not in the containers that hold the data. Exactly one place.

The static_array simply hands out its internal storage as a slice:

constexpr Slice<const T> as_slice() const noexcept {
    return Slice<const T>(data_, N);
}

That is Rust’s &[T] deref coercion in C++ syntax. The consequence: contains, find and sort are written once and usable by any container that can hand out a slice. Add another contiguous container later and it gets search and sort for free, no algorithm code duplicated. static_array::sort() itself is therefore only three lines:

template <typename T, std::size_t N>
constexpr SortedArray<T, N> static_array<T, N>::sort() const {
    auto copy = *this;
    static_cast<void>(copy.as_mut_slice().sort());  // the slice does the work
    return SortedArray<T, N>(std::move(copy));
}

The container makes a copy, hands out a mutable slice over it, and delegates. No sorting algorithm in the container itself. The STL solves this differently: algorithms in <algorithm> as free functions over iterator pairs. A different abstraction with a different trade-off, the length is not bundled in, it follows from end - begin. When you build it yourself you have to commit to one variant and feel in your own code what it costs.

The obvious follow-up: which containers does this even work for? It hangs on a single property, is the memory contiguous or not. From that a clear split follows.

Slice heirs are all containers that allocate contiguously: a fixed-size or growable array, the dense matrices and vectors of a linear-algebra type, the slots of an open-addressing hash table. Whatever the structure, as long as its elements sit back to back in memory it can hand out a slice and inherits every algorithm for free.

Adapters like stack and queue build on a container and deliberately restrict the interface. They do not hand out a slice, because push, pop and top make sense precisely when the container is not sortable and not freely indexable. A stack that hands out a slice is no longer a stack. This is the point where the rule “write once, use everywhere” deliberately stops.

Iterator containers are the structures whose memory is not contiguous: linked lists, trees without an array backing, graphs with adjacency lists, hash maps with separate chaining. Here a fat pointer is pointless, because it would have to point at several scattered regions. Instead of a slice an iterator steps in, closing off the layer downward cleanly. Rust shows the same convention: contiguous containers implement Deref<Target=[T]> and thereby hand out &[T], pointer-backed structures only offer an Iterator. The two are not mutually exclusive: Vec<T> has both, because its iterator is internally the same pointer arithmetic over the same memory.

From that follows the question I ask myself for every new container: array-backed or pointer-backed? Array-backed means slice for free, cache-friendly, with size limits. Pointer-backed means iterator API, more flexible. Both are legitimate, mixing them is the trap: a non-contiguous container that hands out a slice lies about its memory layout. This question has become the first one I ask, before I even start writing a new container.

What surprised me most along the way is how far the principle stretches. static_array::sort() returns no sorted static_array, but a new type:

[[nodiscard]] constexpr SortedArray<T, N> sort() const;

And binary_search is available only on that type:

template <typename T, std::size_t N>
class SortedArray {
  public:
    constexpr BinarySearchResult binary_search(const T& value) const noexcept;
    // sort() does not exist here anymore
};

binary_search on an unsorted array silently violates the precondition of the algorithm: no compiler error, no crash, just a wrong result. The STL solves this by convention: you just sort beforehand, and the compiler does not help. In my implementation binary_search is simply not a member of static_array. It is only available on SortedArray:

arr.binary_search(42);          // compile error: no member
arr.sort().binary_search(42);   // OK

That is exactly the point where the type system makes an assumption of the program logic visible. Rust’s [T]::binary_search() shows the alternative: the method exists on every slice, the precondition lies with the caller, the compiler does not check it. The newtype approach, which enforces the same contract at the type level, you find in Rust as a deliberate design pattern, not as a stdlib feature. Building it yourself, you grasp why that is so. Using the STL, it stays a good tip from a book.

Both observations are ultimately one: build the right abstraction layer, and the algorithms come for free. Not because my 76-line Slice class is better than std::span. But because at every line you make a design decision that stays invisible in the STL. Building the slice myself was the fastest way not just to know Rust’s &[T] deref coercion, but to understand why it exists.