Consider this code that attempts to create various std::span
objects for a vector of raw pointers.
#include <vector>
#include <span>
int main()
{
struct S {};
std::vector<S*> v;
std::span<S*> span1{v};
std::span<S* const> span2{v};
std::span<const S* const> span3{v};
std::span<const S*> span4{v};
return 0;
}
span3
compiles fine, but span4
fails with the following error:
<source>: In function 'int main()':
<source>:58:32: error: no matching function for call to 'std::span<const main()::S*>::span(<brace-enclosed initializer list>)'
58 | std::span<const S*> span4{v};
| ^
In file included from /opt/compiler-explorer/gcc-12.2.0/include/c /12.2.0/ranges:45,
from <source>:5:
/opt/compiler-explorer/gcc-12.2.0/include/c /12.2.0/span:231:9: note: candidate: 'template<class _OType, long unsigned int _OExtent> requires (_Extent == std::dynamic_extent || _OExtent == std::dynamic_extent || _Extent == _OExtent) && (std::__is_array_convertible<_Type, _Tp>::value) constexpr std::span<_Type, _Extent>::span(const std::span<_OType, _OExtent>&) [with long unsigned int _OExtent = _OType; _Type = const main()::S*; long unsigned int _Extent = 18446744073709551615]'
I would either expected span3
and span4
both to fail or both to succeed. Can someone explain why logical constness can be added to to a std::span
of raw pointers iff the underlying pointer is const
(i.e. bitwise).
CodePudding user response:
std::span<const S*>
allows you to assign aconst S*
to an element.std::vector<S*>
allows you to read an element of typeS*
.- If
std::span<const S*>
were allowed to take astd::vector<S*>
, then it would be possible to sneakily convert aconst S*
to aS*
, by assigning theconst S*
to an element of the span and then reading the same element through the vector.
That is to say, if std::span<const S*> span4{v};
were allowed, then the following program would be valid:
#include <vector>
#include <span>
int main()
{
struct S { int value; };
std::vector<S*> v = {nullptr};
std::span<const S*> span4{v}; // Note: not allowed in reality
const S s{.value = 0};
span4[0] = &s;
S* p = v[0];
assert(p == &s);
p->value = 42; // Oops, modifies the value of a const object!
}
So in order to prevent this scenario and provide const-correctness, std::span<const S*>
must not be constructible from a std::vector<S*>
.
On the other hand, std::span<const S* const>
does not allow you to assign to its elements, so it's safe for it to take a std::vector<S*>
.
(Yes, it's the same reason that you can convert a S**
to const S* const *
, but you cannot convert it to const S**
.)
CodePudding user response:
The range version of the span
constructor has the following constraints:
is_convertible_v<U(*)[], element_type(*)[]>
istrue
.[Note 5: The intent is to allow only qualification conversions of the range reference type to
element_type
. — end note]
where U
is remove_reference_t<ranges::range_reference_t<R>>
, which in your case is S*
, and element_type
is the template type of span
.
Then we can get the following
struct S {};
template<class U, class element_type>
concept array_convertible = std::is_convertible_v<U(*)[], element_type(*)[]>;
static_assert(array_convertible<S*, S*>);
static_assert(array_convertible<S*, S* const>);
static_assert(array_convertible<S*, const S* const>);
static_assert(array_convertible<S*, const S*>); // static assertion failed
Given that the constraints are not satisfied, using std::vector<S*>
to initialize std::span<const S*>
is ill-formed.