Document number: | P1976R1 | |
---|---|---|
Date: | 2020-01-03 | |
Audience: | Library Working Group | |
Reply-to: | Tomasz Kamiński <tomaszkam at gmail dot com> |
span
construction from dynamic rangeThis paper provides more detailed explanation of PL250 NB issue.
We explore issues with construction of fixed-size span
construction from the range
with the dynamic size. This constructor are source of the undefined behavior, without providing
any synctatic suggestion on the user side.
To resolve the issues, we present three options:
span
(remove fixed-size span
for C++20)Per LEWG guidance in Belfast, the proposed resolution follows the option C (PL250 guidance) and marks the fixed-spize span
constructors
from dynamic-size range explicit.
Before | After | |
---|---|---|
void processFixed(std::span<int, 5>); void processDynamic(std::span<int>); std::vector<int> v3(3); std::vector<int> v5(5); |
||
Dynamic range with different size (5 vs 3) |
||
processFixed(v3); // processFixed({v3.data(), v3.data() + 3}); // processFixed(span<int, 5>(v3)); // processFixed(span<int, 5>{v3.data(), v3.data() + 3}); // processFixed(span<int>(v3).first<5>()); // processFixed(span<int>{v3.data(), v3.data() + 3}.first<5>()); // span<int, 5> s = v3; // span<int, 5> s(v3); // span<int, 5> s = {v3.data(), v3.data() + 3}; // span<int, 5> s{v3.data(), v3.data() + 3}; // |
ill-formed undefined-behavior ill-formed undefined-behavior undefined-behavior undefined-behavior ill-formed undefined-behavior undefined-behavior undefined-behavior |
ill-formed ill-formed undefined-behavior undefined-behavior undefined-behavior undefined-behavior ill-formed undefined-behavior ill-formed undefined-behavior |
Dynamic range with matching size (5 vs 5) |
||
processFixed(v5); // processFixed({v5.data(), v5.data() + 5}); // processFixed(span<int, 5>(v5)); // processFixed(span<int, 5>{v5.data(), v5.data() + 5}); // processFixed(span<int>(v5).first<5>()); // processFixed(span<int>{v5.data(), v5.data() + 5}.first<5>()); // span<int, 5> s = v5; // span<int, 5> s(v5); // span<int, 5> s = {v5.data(), v5.data() + 5}; // span<int, 5> s{v5.data(), v5.data() + 5}; // |
ill-formed ok ill-formed ok ok ok ill-formed ok ok ok |
ill-formed ill-formed ok ok ok ok ill-formed ok ill-formed ok |
Initial revision.
The resolution of the LWG issue 3101 prevents user from running
into accidental undefined-behavior when the span
with fixed size is constructed from the
range with the size that is not known at compile time. To illustrate:
void processFixed(std::span<int, 5>); std::vector<int> v;
With the above declaration the following invocation is ill-formed:
processFixed(v); // ill-formed
Before the resolution of the issues, the above code was having undefined-behavior if the v.size()
was
different than 5
(size of span
in declaration of processFixed
).
However, the proposed resolution does not prevent the accidental undefined-behavior in situation when
(iterator, size)
or the (iterator, sentinel)
constructor is used:
void processFixed({v.data(), v.size()}); // undefined-behavior if v.size() != 5 void processFixed({v.begin(), v.end()}); // undefined-behavior if v.size() != 5
span
(remove fixed-size span
for C++20)One of the option of resolving the issue is to separate the fixed-size and dynamic-size span
into separate template. As it is to late for the C++20 for the introduction of the new template,
such change would imply removal of the fixed-size span
version of the span
from the standard.
As consequence, the span
template would become dynamicly sized, and would accept
single type as template parameter:
template<class T> span;
Futhermore it would allow us to explore extending fixed-span
construction
to handle user-defined fixed-size ranges. Currently the standard regonizes only native arrays (T[N]
),
std::array<T, N>
and fixed-size std::span<T, N>
(where N != std::dynamic_extent
)
as fixed-size range. The appropariate trait was proposed in
A SFINAE-friendly trait to determine the extent of statically sized containers.
We can follow the direction of the LWG issue 3101 and
disable these constructor from particpating from the overload resolution entirelly. That would
prevent the constructing the fixed-span from the dynamic range, and require the
user to first<N>()
/last<N>
/subspan<P, N>
methods explicitly.
void processFixed(std::span(v).first<5>()); // undefined-behavior if v.size() < 5 void processFixed(std::span(v).last<5>()); // undefined-behavior if v.size() < 5 void processFixed(std::span(v).subspan<1, 5>()); // undefined-behavior if v.size() < 6 = 1 + 5
[ Note: Lack of template parameter for span
in above examples is intentional - they use deduction guides. ]
Tony Tables for option B.
Before | After: Option B |
---|---|
void processFixed(std::span<int, 5>); void processDynamic(std::span<int>); |
|
Dynamic range with different size |
|
std::vector<int> v3(3); processFixed(v3); // ill-formed processFixed({v3.data(), v3.data() + 3}); // undefined-behavior processFixed({v3.data(), 3}); // undefined-behavior processFixed(span<int, 5>(v3)); // ill-formed processFixed(span<int, 5>{v3.data(), v3.data() + 3}); // undefined-behavior processFixed(span<int, 5>{v3.data(), 3}); // undefined-behavior processFixed(span<int>(v3).first<5>()); // undefined-behavior processFixed(span<int>{v3.data(), v3.data() + 3}.first<5>()); // undefined-behavior processFixed(span<int>{v3.data(), 3}.first<5>()); // undefined-behavior |
processFixed(v3); // ill-formed processFixed({v3.data(), v3.data() + 3}); // ill-formed processFixed({v3.data(), 3}); // ill-formed processFixed(span<int, 5>(v3)); // ill-formed processFixed(span<int, 5>{v3.data(), v3.data() + 3}); // ill-formed processFixed(span<int, 5>{v3.data(), 3}); // ill-formed processFixed(span<int>(v3).first<5>()); // undefined-behavior processFixed(span<int>{v3.data(), v3.data() + 3}.first<5>()); // undefined-behavior processFixed(span<int>{v3.data(), 3}.first<5>()); // undefined-behavior |
Dynamic range with matching size |
|
std::vector<int> v5(5); processFixed(v5); // ill-formed processFixed({v5.data(), v5.data() + 5}); // ok processFixed({v5.data(), 5}); // ok processFixed(span<int, 5>(v5)); // ill-formed processFixed(span<int, 5>{v5.data(), v5.data() + 5}); // ok processFixed(span<int, 5>{v5.data(), 5}); // ok processFixed(span<int>(v5).first<5>()); // ok processFixed(span<int>{v5.data(), v5.data() + 5}.first<5>()); // ok processFixed(span<int>{v5.data(), 5}.first<5>()); // ok |
processFixed(v5); // ill-formed processFixed({v5.data(), v5.data() + 5}); // ill-formed processFixed({v5.data(), 5}); // ill-formed processFixed(span<int, 5>(v5)); // ill-formed processFixed(span<int, 5>{v5.data(), v5.data() + 5}); // ill-formed processFixed(span<int, 5>{v5.data(), 5}); // ill-formed processFixed(span<int>(v5).first<5>()); // ok processFixed(span<int>{v5.data(), v5.data() + 5}.first<5>()); // ok processFixed(span<int>{v5.data(), 5}.first<5>()); // ok |
This is original resolution proposed in PL250.
The construction of the fixed-sized span
from the dynamicly sized range, is
not indentity operation - this operation assumes additional semantic property of the type
(size of the range). Such conversion between semantically different types, should not be
implicit. We can resolve the problem, by marking all of such constructor explicit, as follows:
Destination/Source | Fixed | Dynamic |
---|---|---|
Fixed | implicit (ill-formed if source.size() != dest.size()) | explicit (undefined-behavior if source.size() != dest.size()) |
Dynamic | implicit (always ok) | implicit (always ok) |
Tony Tables for option C.
Before | After: Option C |
---|---|
void processFixed(std::span<int, 5>); void processDynamic(std::span<int>); |
|
Dynamic range with different size |
|
std::vector<int> v3(3); processFixed(v3); // ill-formed processFixed({v3.data(), v3.data() + 3}); // undefined-behavior processFixed({v3.data(), 3}); // undefined-behavior processFixed(span<int, 5>(v3)); // ill-formed processFixed(span<int, 5>{v3.data(), v3.data() + 3}); // undefined-behavior processFixed(span<int, 5>{v3.data(), 3}); // undefined-behavior processFixed(span<int>(v3).first<5>()); // undefined-behavior processFixed(span<int>{v3.data(), v3.data() + 3}.first<5>()); // undefined-behavior processFixed(span<int>{v3.data(), 3}.first<5>()); // undefined-behavior |
processFixed(v3); // ill-formed processFixed({v3.data(), v3.data() + 3}); // ill-formed processFixed({v3.data(), 3}); // ill-formed processFixed(span<int, 5>(v3)); // undefined-behavior processFixed(span<int, 5>{v3.data(), v3.data() + 3}); // undefined-behavior processFixed(span<int, 5>{v3.data(), 3}); // undefined-behavior processFixed(span<int>(v3).first<5>()); // undefined-behavior processFixed(span<int>{v3.data(), v3.data() + 3}.first<5>()); // undefined-behavior processFixed(span<int>{v3.data(), 3}.first<5>()); // undefined-behavior |
Dynamic range with matching size |
|
std::vector<int> v5(5); processFixed(v5); // ill-formed processFixed({v5.data(), v5.data() + 5}); // ok processFixed({v5.data(), 5}); // ok processFixed(span<int, 5>(v5)); // ill-formed processFixed(span<int, 5>{v5.data(), v5.data() + 5}); // ok processFixed(span<int, 5>{v5.data(), 5}); // ok processFixed(span<int>(v5).first<5>()); // ok processFixed(span<int>{v5.data(), v5.data() + 5}.first<5>()); // ok processFixed(span<int>{v5.data(), 5}.first<5>()); // ok |
processFixed(v5); // ill-formed processFixed({v5.data(), v5.data() + 5}); // ill-formed processFixed({v5.data(), 5}); // ill-formed processFixed(span<int, 5>(v5)); // ok processFixed(span<int, 5>{v5.data(), v5.data() + 5}); // ok processFixed(span<int, 5>{v5.data(), 5}); // ok processFixed(span<int>(v5).first<5>()); // ok processFixed(span<int>{v5.data(), v5.data() + 5}.first<5>()); // ok processFixed(span<int>{v5.data(), 5}.first<5>()); // ok |
All proposed options (including removal) does not have any impact on the construction of the
dynamic-sized span (i.e. span<T>
). The construction changes affect only
cases when N != std::dynamic_extent
.
The major difference between the option B and option C, is the impact the impact on the initialization of the span variables. Some of the readers, may consider the difference between various syntaxes and their meaning two subtle.
Tony Tables for initialization.
Option B | Option C |
---|---|
std::vector<int> v3(3); span<int, 5> s = v3; // ill-formed span<int, 5> s(v3); // ill-formed auto s = span<int, 5>(v3); // ill-formed span<int, 5> s = {v3.data(), v3.data() + 3}; // ill-formed span<int, 5> s{v3.data(), v3.data() + 3}; // ill-formed auto s = span<int, 5>{v3.data(), v3.data() + 3}; // ill-formed |
span<int, 5> s = v3; // ill-formed span<int, 5> s(v3); // undefined-behavior auto s = span<int, 5>(v3); // undefined-behavior span<int, 5> s = {v3.data(), v3.data() + 3}; // ill-formed span<int, 5> s{v3.data(), v3.data() + 3}; // undefined-behavior auto s = span<int, 5>{v3.data(), v3.data() + 3}; // undefined-behavior |
Neither option B nor C, proposes any change to the behavior of the construction of
the fixed-size span
from the ranges that are recognized by the
standard as fixed-size: native arrays (T[N]
),
std::array<T, N>
and fixed-size std::span<T, N>
(where N != std::dynamic_extent
).
The construction is implicit if size of the source is the same as the size of destination,
ill-formed otherwise.
void processFixed(span<int, 5>); std::array<int, 3> a3; std::array<int, 5> a5; processFixed(a3); // ill-formed processFixed(a5); // ok std::span<int, 3> s3(a3); std::span<int, 5> s5(a5); processFixed(s3); // ill-formed processFixed(s5); // ok
The P1394: Range constructor for std::span
(that is targeting C++20) generalized the constructor of the span.
The Container
constructor was replaced with the Range
constructor,
that have the same constrain (i.e. it is disabled for fixed-size span
),
so the original example remain ill-formed:
processFixed(v); // ill-formed
In addition it replaces the (pointer, size)
and (pointer, pointer)
constructor, with more general (iterator, size)
and (iterator, sentinel)
.
As consequence in addition the undefined-behavior is exposed in more situations:
void processFixed({v.begin(), v.size()}); // undefined-behavior if v.size() != 5 void processFixed({v.begin(), v.end()}); // undefined-behavior if v.size() != 5
in addition to:
void processFixed({v.data(), v.size()}); // undefined-behavior if v.size() != 5 void processFixed({v.data(), v.data() + v.size()}); // undefined-behavior if v.size() != 5
Changes presented in this paper still apply after signature changes from P1394.
As the std:span
was introduced in C++20, the changes introduce in these paper (regardless of the selected option)
cannot break existing code. In addition, all presented options do not affect uses of span
with the dynamic size.
The implementation of the option B requires duplicating a constrain:
Constrains: extent == dynamic_extent
is true
.
that is already present in Container
/Range
constructor
([span.cons] p14.1) to 3 additional constuctors.
In can be implemented using the SFINAE tricks (std::enable_if
) or requires
clause.
The implementation of the option C mostly requires adding an conditional explicit specifier to 4 constuctors:
explicit(extent != dynamic_extent)
The proposed wording changes refer to N4842 (C++ Working Draft, 2019-11-27).
Apply following changes to section [span.overview] Overview:
// [span.cons], constructors, copy, and assignment constexpr span() noexcept; template<class It> constexpr explicit(extent != dynamic_extent) span(It first, size_type count); template<class It, class End> constexpr explicit(extent != dynamic_extent) span(It first, End last); template<size_t N> constexpr span(element_type (&arr)[N]) noexcept; template<size_t N> constexpr span(array<value_type, N>& arr) noexcept; template<size_t N> constexpr span(const array<value_type, N>& arr) noexcept; template<class R> constexpr explicit(extent != dynamic_extent) span(R&& r); constexpr span(const span& other) noexcept = default; template<class OtherElementType, size_t OtherExtent> constexpr explicit(see below) span(const span<OtherElementType, OtherExtent>& s) noexcept;
Apply following changes to section [span.cons] Constructors, copy, and assignment:
template<class It> constexpr explicit(extent != dynamic_extent) span(It first, size_type count);
- Constraints:
- Let
U
beremove_reference_t<iter_reference_t<It>>
.
It
satisfiescontiguous_iterator
.is_convertible_v<U(*)[], element_type(*)[]>
istrue
. [Note: The intent is to allow only qualification conversions of the iterator reference type toelement_type
. — end note]- Preconditions:
[first, first + count)
is a valid range.It
modelscontiguous_iterator
.- If
extent
is not equal todynamic_extent
, thencount
is equal toextent
.- Effects:
- Initializes
data_
withto_address(first)
andsize_
withcount
.- Throws:
- When and what
to_address(first)
throws.template<class It, class End> constexpr explicit(extent != dynamic_extent) span(It first, End last);
- Constraints:
- Let
U
beremove_reference_t<iter_reference_t<It>>
.
is_convertible_v<U(*)[], element_type(*)[]>
istrue
. [Note: The intent is to allow only qualification conversions of the iterator reference type toelement_type
. — end note]It
satisfiescontiguous_iterator
.End
satisfiessized_sentinel_for<It>
.is_convertible_v<End, size_t>
isfalse
.- Preconditions:
- If
extent
is not equal todynamic_extent
, thenlast - first
is equal toextent
.[first, first + count)
is a valid range.It
modelscontiguous_iterator
.End
modelssized_sentinel_for<It>
.- Effects:
- Initializes
data_
withto_address(first)
andsize_
withlast - first
.- Throws:
- When and what
to_address(first)
throws.template<size_t N> constexpr span(element_type (&arr)[N]) noexcept; template<size_t N> constexpr span(array<value_type, N>& arr) noexcept; template<size_t N> constexpr span(const array<value_type, N>& arr) noexcept;
- Constraints:
- Let
U
beremove_pointer_t<decltype(data(arr))>
.
extent == dynamic_extent || N == extent
istrue
, andremove_pointer_t<decltype(data(arr))>(*)[]
is convertible toElementType(*)[]
.is_convertible_v<U(*)[], element_type(*)[]>
istrue
. [Note: The intent is to allow only qualification conversions of the iterator reference type toelement_type
. — end note]- Effects:
- Constructs a
span
that is a view over the supplied array..- Postconditions:
size() == N && data() == data(arr)
template<class R> constexpr explicit(extent != dynamic_extent) span(R&& r);[...]
- Constraints:
- Let
U
beremove_reference_t<ranges::range_reference_t<R>>
.
extent == dynamic_extent
istrue
.R
satisfiesranges::contiguous_range
andranges::sized_range
.- Either
R
satisfiessafe_range
oris_const_v<element_type>
istrue
.remove_cvref_t<R>
is not a specialization ofspan
.remove_cvref_t<R>
is not a specialization ofarray
.is_array_v<remove_cvref_t<R>>
isfalse
.is_convertible_v<U(*)[], element_type(*)[]>
istrue
. [Note: The intent is to allow only qualification conversions of the iterator reference type toelement_type
. — end note]- Preconditions:
- If
extent
is not equal todynamic_extent
, thenranges::size(r)
is equal toextent
.R
modelsranges::contiguous_range
andranges::sized_range
.- If
is_const_v<element_type>
isfalse
,R
modelssafe_range
.- Effects:
- Initializes
data_
withranges::data(r)
andsize_
withranges::size(r)
.- Throws:
- What and when
ranges::data(r)
andranges::size(r)
throws.template<class OtherElementType, size_t OtherExtent> constexpr explicit(see below) span(const span<OtherElementType, OtherExtent>& s) noexcept;
- Constraints:
is
Extentextent == dynamic_extent || OtherExtent == dynamic_extent ||Extentextent == OtherExtenttrue
, andOtherElementType(*)[]
is convertible toElementType(*)[]
.is_convertible_v<OtherElementType(*)[], element_type(*)[]>
istrue
. [Note: The intent is to allow only qualification conversions of the iterator reference type toelement_type
. — end note]- Preconditions:
- If
extent
is not equal todynamic_extent
, thens.size()
is equal toextent
.- Effects:
- Constructs a
span
that is a view over the range[s.data(), s.data() + s.size())
.- Postconditions:
size() == other.size() && data() == other.data()
- Remarks:
- The expression inside
explicit
is equivalent to:extent != dynamic_extent && OtherExtent == dynamic_extent
.
Update the value of the __cpp_lib_span
in [version.syn] Header <version>
synopsis to reflect the date of approval of this proposal.
Andrzej Krzemieński, Casey Carter, Tim Song and Jeff Garland offered many useful suggestions and corrections to the proposal.
Special thanks and recognition goes to Sabre (http://www.sabre.com) for supporting the production of this proposal and author's participation in standardization committee.
std::span
",
(P1394R4, https://wg21.link/p1394r4)