The standard containers propagate const. That is, their elements are automatically const if the containers themselves are const. For example:
const std::vector vec{3, 1, 4, 1, 5, 9, 2, 6};
ranges::fill(vec, 314); // impossible
const std::list lst{2, 7, 1, 8, 2, 8, 1, 8};
ranges::fill(lst, 272); // impossible
Builtin arrays also propagate const:
const int arr[] {1, 4, 1, 4, 2, 1, 3, 5};
ranges::fill(arr, 141); // impossible
However, I noticed that std::span
(presumably) does not propagate const. Minimal Reproducible Example:
#include <algorithm>
#include <cassert>
#include <span>
namespace ranges = std::ranges;
int main()
{
int arr[] {1, 7, 3, 2, 0, 5, 0, 8};
const std::span spn{arr};
ranges::fill(spn, 173); // this compiles
assert(ranges::count(arr, 173) == 8); // passes
}
Why does this code work fine? Why does std::span
treat const differently than the standard containers?
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
.
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>
.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With