| 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
Uberemove_reference_t<iter_reference_t<It>>.
Itsatisfiescontiguous_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.
Itmodelscontiguous_iterator.- If
extentis not equal todynamic_extent, thencountis 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
Uberemove_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]
Itsatisfiescontiguous_iterator.
Endsatisfiessized_sentinel_for<It>.
is_convertible_v<End, size_t>isfalse.- Preconditions:
- If
extentis not equal todynamic_extent, thenlast - firstis equal toextent.
[first, first + count)is a valid range.
Itmodelscontiguous_iterator.
Endmodelssized_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
Uberemove_pointer_t<decltype(data(arr))>.
extent == dynamic_extent || N == extentistrue, and
remove_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
spanthat 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
Uberemove_reference_t<ranges::range_reference_t<R>>.
extent == dynamic_extentistrue.
Rsatisfiesranges::contiguous_rangeandranges::sized_range.- Either
Rsatisfiessafe_rangeoris_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
extentis not equal todynamic_extent, thenranges::size(r)is equal toextent.
Rmodelsranges::contiguous_rangeandranges::sized_range.- If
is_const_v<element_type>isfalse,Rmodelssafe_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:
isExtentextent == dynamic_extent || OtherExtent == dynamic_extent ||Extentextent == OtherExtenttrue, and
OtherElementType(*)[]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
extentis not equal todynamic_extent, thens.size()is equal toextent.- Effects:
- Constructs a
spanthat is a view over the range[s.data(), s.data() + s.size()).- Postconditions:
size() == other.size() && data() == other.data()- Remarks:
- The expression inside
explicitis 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)