Document number: |
P2447R0 |
Date: |
2021-10-14 |
Project: |
Programming Language C++, Library Working Group |
Reply-to: |
1. Motivation
Given the functions signature void foo(std::span<const int>);
, how to call foo
with a constant set of values?
before |
after |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The fact that it is not possible in an easy and natural way to create a span over a set of given values is the main motivation for this paper.
Following pattern:
foo({1,2,3});
does currently not compile, as std::span
does not have a constructor that takes an std::initializer_list
.
Note that in the case of an empty container, foo({});
compiles making the construction of span apparently inconsistent.
The after/before table shows different types of workarounds on how to handle such situation.
The most trivial workaround is creating an array, and pass it to foo
.
This introduces a variable that was not there before, which also has a much bigger scope (which might or might be not an issue), thus this approach has a clear drawback.
Notice that one could also use a local std::initializer_list
, and std::span
range constructor will accept it without issues.
Another approach is creating a temporary container, the easiest one to write ist std::vector
, but doing so introduce unnecessary overhead.
One could of course create a temporary array, which is more subtle than it needs to be, or std::array
.
Another alternative, is writing a helper function, that takes a set of values and converts them to a span.
When using std::array
, the syntax between empty and non-empty std::array
is inconsistent, unless one does not take advantage of class template argument deduction, as foo(std::array{1,2,3})
compiles, but foo(std::array{})
does not.
Writing the size by hand is not really user-friendly, but at least would make the code consistent, as both foo(std::array<int,3>{1,2,3});
and foo(std::array<int,0>{});
compiles.
Last but not least, one can write foo({{1,2,3}})
, which picks the array overload.
This is probably the best/less verbose approach, but the syntax is uncommon, the reasons which overloads gets picked unclear and it is also easy to oversee the second pair of brackets.
But, this syntax is inconsistent when dealing with empty spans, as foo({{}})
does not compile.
Spelling std::initializer_list<int>
out makes the code compile.
As normally one would not write std::initializer_list
when initializing a container, it makes span construction awkward.
To sum it up:
-
in all cases, to call
foo
with a given set of values, it is necessary to manually add some sort of indirection. -
the less verbose approach is the less obvious
-
the simplest approaches (temporary vector, local container) have drawbacks
-
all approaches, except using a helper function, spelling
std::initializer_list
out or usingstd::array
without type deduction, are inconsistent between empty and non-empty spans.
This makes std::span
somewhat more difficult to use.
1.1. Upgrading from vector
While the main motivation for adding another constructor to span has already been presented, the "historical" motivation that led to the creation of this paper is the introduction of std::span
in existing code.
Consider following function signature:
void foo(const std::vector<int>&);
As the parameter is passed by const-reference, and foo
does not need the ownership of the data, using std::vector
is often suboptimal especially since we have std::span
.
"upgrading"/"enhancing" the function signature to
void foo(std::span<const int>);
is a code incompatible change for the user of such function. Even recompiling the whole codebase (thus ignoring ABI issues) can lead to compiler errors.
Even if there are trivial code-transformation for fixing the signature incompatibility it is harder to introduce std::span
in a bigger and older codebase.
The Usability Enhancements for std::span paper, gives another reason why we should have a constructor for std::initializer_list
(emphasis added):
A span is […]. It is intended as a new "vocabulary type" for contiguous ranges, replacing the use of(pointer, length) pairs and, in some cases, vector<T, A>& function parameters.
This can be also read from the original span paper:
To simplify use of span as a simple parameter, span offers a number of constructors for common container types that store contiguous sequences of elements.
Upgrading void foo(const std::vector<int>&);
to void foo(std::span<const int>);
is one of the main use-cases of std::span
.
It is desirable that the transition from one function signature to another is as smooth and error-free as possible.
The current incompatibility adds unnecessary work to the users of the function. Making such change might thus not be possible, especially for a public API.
1.1.1. Does that imply that std::span
should have all constructors of std::vector
(or other container with sequential storage)?
No.
While on could write
void f(const std::vector<std::string>&);
foo({4, "hello"}); // vector with 4 times the string "hello"
if span would support std::initializer_list
such code would still not compile.
Nevertheless, based on personal experience, most temporary vectors (or other containers) are either constructed from a constant set of values (thus with a std::initializer_list
), or empty.
Thus while it is true that even if this paper gets accepted the upgrade process from std::vector
to std::span
is still a breaking change, the chances of breaking existing code are much lower.
1.2. Why doesn’t span have a std::initializer_list
constructor?
As a matter of fact it is possible to create a span from an std::initializer_list
by spelling the type out.
The absence of such constructor seems to imply that a span should not be constructed from an std::initializer_list
(as normally one does not spell std::initializer_list
out).
I could not find any rationale for not adding such constructor, except those presented in this issue: https://github.com/Microsoft/GSL/issues/459
1.2.1. dangling spans
The first argument is that if we added such a constructor, it would be easier to create a dangling span accidentally:
std::span<const int> sp = {1, 2, 3}; // dangles immediately
std::span sp{1, 2, 3}; // dangles immediately
However, this argument seems weak, because it is already possible to create a dangling span variable in many ways. For example:
// ---
std::span<const int> sp = std::vector<int>{1,2,3}; // dangles immediately
// ---
// suppose that std::span<const int> bar(); changed to
std::vector<int> bar();
std::span<const int> sp = bar(); // dangles immediately
// ---
std::span<const int> sp;
{
const auto v = std::vector<int>{1,2,3};
sp = v;
}
// dangling sp
rvalues are not necessarily short-lived, and the lifetime of an lvalue might be shorter than the lifetime of the constructed span.
Thus using value categories for determining the life-time, as in this case (if the motivation found on github is the one why span does not have the proposed constructor), gives a false sense of security, and disallows valid use-cases.
std::span
behaves like a pointer and an associated size, as it does not own the resource (just like std::string_view
).
Thus dangling spans are unavoidable, just like dangling references and pointers.
Fortunately it is possible to diagnose those type of errors statically without making span harder to use.
1.2.2. span is a view over a container
The other presented argument is that span is a view over a container, and that std::initializer_list
is not a container.
A std::span
can be constructed from "something" that is contiguous memory (std::ranges::data
and std::ranges::size
needs to work on it).
That "something" does thus not even need to be a container.
Also a pointer and a length are not a container, yet there is a constructor for it, as this is another main use-case for span.
std::initializer_list
might not be defined as a container, but
-
it has contiguous memory
-
it has a container-like interface (member functions
begin
,end
andsize
, not even anarray
has those. It also has free functions forcbegin
,rbegin
, …) -
std::span
can be already constructed from astd::initializer_list
, one must "just" be very explicit about it when writing it in code.
As it is possible to create a span from a std::initializer_list
, and as std::initializer_list
can be used like a container (just like span), the distinction for span between a std::initializer_list
and other containers is artificial.
1.3. This is a breaking change
Unfortunately, adding std::initializer_list
is a breaking change.
Currently, when using { /* … */ }
a constructor not taking std::initializer_list
will be called.
With this paper, depending on the arguments, the constructor taking std::initializer_list
might be called.
As the proposed constructor for std::initializer_list
is constrained for a span over constant elements, the breakage is reduced to a subset of types with unconstrained constructor when creating a span.
For those examples, the code behaviour is unchanged as the constraint rules the new constructor out:
const std::vector<int>;
auto sp1 = std::span{v};
auto sp2 = std::span{v.begin(), v.end()};
auto begin = v.data();
auto end = begin + v.size();
auto sp3 = std::span{v.begin(), v.end()};
To use the std::initializer_list
constructor, one needs to spell the type out:
const std::vector<int>;
auto sp1 = std::span<const std::vector<int>>{v};
Types with unconstrained constructors, for example void*
and std::any
, are those who are affected by the new constructor:
void foo(span<void* const> s);
// without this paper, creates a span with one void*
// with this paper, creates a span of two void*
void* vp = nullptr;
span<void* const> sp{&vp, &vp+1};
// without this paper, creates a span with one std::any*
// with this paper, creates a span of two elements
std::any a;
span<std::any> sa{&ap, &ap+1};
One of the main use-case for span is being used as function parameter:
One of the major advantages of span over the common idiom of a “pointer plus length” pair of parameters is that it […]
For those intended use-cases the breaking change will be very uncommon as the type is normally spelled out.
For example, if a function signature would have been
void foo(const std::vector<int*>&);
when changing it to
void foo(std::span<int* const>);
then
int* ptr1 = ...;
int* ptr2 = ...;
foo({ptr1, ptr2});
can only be interpreted as a std::span<int* const>
constructed with an std::initializer_list<int*>
, and not a std::span<int* const>
created from a pair of iterators.
If the type is not spelled out, as in
template <class T>
void foo(span<conts T> s){/* ... */}
then class template argument deduction generally will not work:
int* begin = nullptr;
int* end = nullptr;
foo({begin, end});
/*
error: no matching function for call to 'foo(<brace-enclosed initializer list>)'
foo({begin, end});
~~~^~~~~~~~~~~~~~
note: candidate: 'template<class T> void foo(std::span<const T>)'
void foo(std::span<const T> s){}
^~~
note: template argument deduction/substitution failed:
note: couldn't deduce template parameter 'T'
foo({begin, end});
~~~^~~~~~~~~~~~~~
*/
As the expected usage of std::span
is to be used (A) as a parameter type and (B) with a non-deduced template argument, the breaking change (considering types like std::span<void*>
) should not affect much code.
A similar contrived example, involving deduction and braced initializers, was successfully broken by DR between C++14 and C++17:
auto x{1}; // C++14: initializer_list<int>. C++17: int.
Therefore, we hope that we can similarly get away with this breakage.
2. Design Decisions
This is purely a library extension.
It is sufficient to add a constructor for std::initializer_list
to std::span
.
Similarly to the constructors that takes an array, it is unconditionally noexcept
, as none of the operations for creating a std::span
over a std::initializer_list
can fail.
Similarly to other constructors the proposed constructor is explicit
if extent != dynamic_extent
.
As std::initializer_list
provides only constant access to the elements, this constructor is only available for a span over constant elements.
This also reduces the breaking changes to only a small subset of contrived examples.
A reference implementation provided by Arthur O’Dwyer for libc++ can be found on on github, he also provided a playground on godbolt.
3. Proposed Wording Changes
The following proposed wording changes against the working draft of the standard are relative to N4892.
Apply following modifications to Header span synopsis [span.syn]
:
#include <initializer_list> // see [initializer.list.syn] namespace std { // constants inline constexpr size_t dynamic_extent = numeric_limits<size_t>::max();
Apply following modifications to definition of Class template span[views.span]
, Overview [span.overview]
:
template<class ElementType, size_t Extent = dynamic_extent>class span {
public:// constants and types
using element_type = ElementType;
using value_type = remove_cv_t<ElementType>;
using size_type = size_t;
using difference_type = ptrdiff_t;
using pointer = element_type*;
using const_pointer = const element_type*;
using reference = element_type&;
using const_reference = const element_type&;
using iterator =implementation-defined;// see 22.7.3.7
using reverse_iterator = std::reverse_iterator<iterator>;
static constexpr size_type extent = Extent;
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(type_identity_t<element_type> (&arr)[N]) noexcept;
template<class T, size_t N>
constexpr span(array<T, N>& arr) noexcept;
template<class T, size_t N>
constexpr span(const array<T, N>& arr) noexcept;
template<class R>
constexpr explicit(extent != dynamic_extent) span(R&& r);
constexpr explicit(extent != dynamic_extent) span(std::initializer_list<value_type> il) noexcept;
constexpr span(const span& other) noexcept = default;
Add following text to Constructors, copy, and assignment [span.cons]
constexpr explicit(extent != dynamic_extent) span(std::initializer_list<value_type> il) noexcept;
Constrains: is_const_v<element_type>
is true
.
Preconditions: If extent
is not equal to dynamic_extent
, then il.size()
is equal to extent
.
Effects: Initializes data_
with il.begin()
and size_
with il.size()
.
4. Acknowledgements
A big thank you to all those giving feedback for this paper.
Especially Arthur O’Dwyer, Barry Revzin, Jonathan Wakely and Tomasz Kamiński for helping with the wording.