Does span propagate const?

Think of pointers. Pointers do not propagate const either. The constness of the pointer is independent from the constness of the element type.

Considered the modified Minimal Reproducible Example:

#include <algorithm>
#include <cassert>
#include <span>

namespace ranges = std::ranges;

int main()
{
    int var = 42;

    int* const ptr{&var};
    ranges::fill_n(ptr, 1, 84); // this also compiles

    assert(var == 84);          // passes
}

It is by design that std::span is kind of a pointer to a contiguous sequence of elements. Per [span.iterators]:

constexpr iterator begin() const noexcept;
constexpr iterator end() const noexcept;

Note that begin() and end() return a non-const iterator regardless of whether the span itself is const or not. Thus, std::span does not propagate const, in a way that is analogous to pointers. The constness of the span is independent from the constness of the element type.

const1 std::span<const2 ElementType, Extent>

The first const specifies the constness of the span itself. The second const specifies the constness of the elements. In other words:

      std::span<      T> // non-const span of non-const elements
      std::span<const T> // non-const span of     const elements
const std::span<      T> //     const span of non-const elements
const std::span<const T> //     const span of     const elements

If we change the declaration of spn in the Example to:

std::span<const int, 8> spn{arr};

The code fails to compile, just like the standard containers. It doesn't matter whether you mark spn itself as const in this regard. (You can't do things like spn = another_arr, though, if you mark it as const)

(Note: you can still use class template argument deduction with the help of std::as_const:

std::span spn{std::as_const(arr)};

Just don't forget to #include <utility>.)


Propagating const for a type like span doesn't actually make much sense, since it cannot protect you from anything anyway.

Consider:

void foo(std::span<int> const& s) {
    // let's say we want this to be ill-formed
    // that is, s[0] gives a int const& which
    // wouldn't be assignable
    s[0] = 42;

    // now, consider what this does
    std::span<int> t = s;

    // and this
    t[0] = 42;
}

Even if s[0] gave an int const&, t[0] surely gives an int&. And t refers to the exactly same elements as s. It's a copy after all, and span doesn't own its elements - it's a reference type. Even if s[0] = 42 failed, std::span(s)[0] = 42 would succeed. This restriction wouldn't do anyone any good.

The difference with the regular containers (e.g. vector) is that the copies here still refer to the same elements, whereas copying a vector would give you entirely new elements.

The way to have span refer to immutable elements isn't to make the span itself const, it's to make the underlying elements themselves const. That is: span<T const>, not span<T> const.