Document number: | P1976R0 | |
---|---|---|
Date: | 2019-11-08 | |
Audience: | Library Evolution 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 tree 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.
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 know 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 makrking 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 pesented options do not affect uses of span
with the dynamic size.
The implementation of the option A 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 B mostly requires adding an conditional explicit specifier to 4 constuctors:
explicit(extent != dynamic_extent)
To be created after specific option is selected.
Andrzej Krzemieński 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)