1. Changelog
-
R3:
-
Changed primary authorship from Federico Kircheis to Arthur O’Dwyer.
-
Removed R2’s feature macro
; it didn’t seem motivated.__cpp_lib_span_init
-
-
R2:
-
Discussed in LEWG telecon, 2022-07-26
-
2. Background
C++17 added
as a "view" over constant string data. Its main purpose
is as a lightweight drop-in replacement for
function parameters.
C++14
| C++17
|
|
|
|
|
|
|
|
|
C++20 added
as a "view" over constant contiguous data of type
(such as
arrays and vectors). One of its main purposes (although not its only one) is as a
lightweight drop-in replacement for
function parameters.
C++17
| C++20
|
|
|
|
|
| |
|
|
This table has a conspicuous gap. The singly-braced initializer list
is implicitly convertible to
, but not to
.
3. Solution
We propose simply that
should be convertible from an appropriate braced-initializer-list. In practice this means adding a constructor from
.
3.1. Implementation experience
This proposal has been implemented in Arthur’s fork of libc++ since October 2021.
See "
should have a converting constructor from
" (2021-10-03) and [Patch].
3.2. What about dangling?
, like
, is specifically designed to bind to rvalues as well as lvalues.
This is what lets us write useful code like:
int take ( std :: string_view s ); std :: string give_string (); int x = take ( give_string ()); int take ( std :: span < const int > v ); std :: vector < int > give_vector (); int x = take ( give_vector ());
Careless misuse of
and
outside a function parameter list can dangle:
std :: string_view s = give_string (); // dangles std :: span < const int > v = give_vector (); // dangles
P2447 doesn’t propose to increase the risk in this area; dangling is already likely when
or
is carelessly misused. We simply propose to close the ergonomic syntax gap between
and
.
Before | After P2447 |
|
|
|
|
3.3. Relation to P2752 Static storage for braced initializers
[P2752R1] (R0)
proposes to permit a constant
like
to refer to
a backing array in static storage, rather than forcing all backing arrays onto the stack.
At least for now, this doesn’t change anything dangling-wise: referring to the backing array
of an
outside the lifetime of that
remains undefined behavior.
std :: string_view s = "abc" ; // OK, no dangling std :: span < const int > v1 = { 1 , 2 , 3 }; // dangles, even after P2752
3.4. Why not just double the braces?
Since we can already write
then why not call that "good enough"? Why do we need to be able to use a single set of braces?std :: span < const int > v = {{ 1 , 2 , 3 }}; // dangles
Well, a single set of braces is good enough for
, and we want
to be a drop-in replacement
for
in function parameter lists, so we need to support the syntax
does.
There was a period right after C++11 where some people were writing
but by C++14 we had settled firmly on "one set of braces" as the preferred style (matching the preferred style for C arrays, pairs, tuples, etc.)std :: vector < int > v = {{ 1 , 2 , 3 }};
So I prefer to turn the question around and say: Since we can already implicitly treat
as a
, how could there be any additional harm in treating
as a
?
This also relates to [P2752R1]: Today,
converts to
by materializing
a temporary of type
on the stack. Tomorrow, if P2447 is adopted,
will convert to
via an
that refers to a backing array also allocated on the stack. [P2752R1] proposes to performance-optimize the latter case (permitting the backing array to occupy
static storage) but not the former case (keeping the status quo for materialized array temporaries).
In other words, tomorrow’s (single-braced or double-braced)
constructor is
"more optimizable" than today’s double-braced array-temporary constructor.
The following program (Godbolt) shows how P2447 lets us benefit from P2752’s optimization:
int perf ( std :: span < const int > ); int test () { return perf ({{ 1 , 2 , 3 }}); }
|
| |
Today | Array on stack | Ill-formed |
---|---|---|
P2752 only | Array on stack | Ill-formed |
P2447 only | IL on stack | IL on stack |
P2447+P2752 | IL in rodata, tail-call | IL in rodata, tail-call |
4. Annex C examples
This change will, of course, break some code (most of it pathological). We might want to add some of these examples to Annex C.
However, any change to overload sets (particularly the addition of new
non-
constructors) can break code.
For example, there was nothing wrong with C++23’s adopting [P1425] "Iterator-pair constructors for
and
"
with no change to Annex C, despite its breaking code like this:
Before: Callsvoid zero ( queue < int > ); void zero ( pair < int * , int *> ); int a [ 10 ]; void test () { zero ({ a , a + 10 }); }
zero ( pair < int , int > )
.After P1425: Ambiguous.
To fix: Eliminate the ambiguous overloading, or cast the argument to
pair
.
We can simply agree that such examples are sufficiently unlikely in practice, and sufficiently easy to fix, that the benefits of the changed overload set outweigh the costs of running into these examples.
4.1. Overload resolution is affected
Before: Callsvoid one ( pair < int , int > ); void one ( span < const int > ); void test () { one ({ 1 , 2 }); }
one ( pair < int , int > )
.After P2447: Ambiguous.
To fix: Eliminate the ambiguous overloading, or cast the argument to
pair
.
4.2. The initializer_list
ctor has high precedence
Before: Selectsvoid two ( span < const int , 2 > ); void test () { two ({{ 1 , 2 }}); }
span ( const int ( & )[ 2 ])
, which is non-explicit
; success.After P2447: Selects
span ( initializer_list < int > )
, which is explicit
for span < const int , 2 >
; failure.To fix: Replace
{{ 1 , 2 }}
with std :: array { 1 , 2 }
; or, replace span < const int , 2 >
with span < const int >
.4.3. Implicit two-argument construction with a highly convertible value_type
In these two highly contrived examples, the caller deliberately constructs
a
via its iterator-pair constructor implicitly, from a braced initializer of two elements, and furthermore
is implicitly convertible from the iterator type.
These examples strike me as highly contrived: both conditions are unlikely, and their
conjunction is unlikelier still.
Before: Selectsint three ( span < void * const > v ) { return v . size (); } void * a [ 10 ]; int x = three ({ a , 0 });
span ( void ** , int )
; x
is 0.After P2447: Selects
span ( initializer_list < void *> )
; x
is 2.To fix: Replace
{ a , 0 }
with span ( a , 0 )
.
Before: Selectsint four ( span < const any > v ) { return v . size (); } any a [ 10 ]; int y = four ({ a , a + 10 });
span ( any * , any * )
; y
is 10.After P2447: Selects
span ( initializer_list < any > )
; y
is 2.To fix: Replace
{ a , a + 10 }
with span ( a , a + 10 )
.
5. Proposed wording
Modify [span.syn] as follows:
#include <initializer_list>// see [initializer.list.syn]
Modify [span.overview] as follows:
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 il ) noexcept ; constexpr span ( const span & other ) noexcept = default ; template < class OtherElementType , size_t OtherExtent > constexpr explicit ( see below ) span ( const span < OtherElementType , OtherExtent >& s ) noexcept ;
Modify [span.cons] as follows:
constexpr explicit ( extent != dynamic_extent ) span ( std :: initializer_list il ) noexcept ;
Constraints:is
is_const_v < element_type > true
.Preconditions: If
is not equal to
extent , then
dynamic_extent is equal to
il . size () .
extent Effects: Initializes
with
data_ and
il . begin () with
size_ .
il . size ()
6. Acknowledgments
-
Thanks to Federico Kircheis for writing the first drafts of this paper.