1. Changelog
-
R4 (pre-Varna 2023):
-
Reorganize references to [P2752] and fix HTML goofs in proposed wording.
-
-
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. 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
?
3.3.1. Better performance via synergy with P2752
[P2752], sent to EWG for Varna, proposes to let a constant
like
refer to a backing array in static storage, rather than forcing all backing arrays onto the stack.
This doesn’t change anything dangling-wise: referring to the backing array of an
outside that
's lifetime remains undefined behavior.
std :: string_view s = "abc" ; // OK, no dangling std :: span < const int > v1 = { 1 , 2 , 3 }; // dangles, even after P2752
Today,
converts to
by materializing a temporary
on the stack.
Tomorrow, if P2447 is adopted,
will convert to
via an
that
refers to a backing array on the stack. P2752 proposes to performance-optimize the latter
(let the backing array occupy static storage) but not the former (keep the status quo for
materialized array temporaries). In other words, the
constructor we propose
here in P2447 is "more optimizer-friendly" than today’s array-temporary constructor.
This example (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 |
In each row, there’s no performance difference between the single-braced or double-braced form. But the only way to reach the bottom row (tail-call, no stack usage) is to adopt both P2447 and P2752; which by a happy coincidence also permits the single-braced form.
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 replace
{ 1 , 2 }
with std :: pair { 1 , 2 }
.
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 )
or span ( a , a )
.
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 < value_type > 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 < value_type > 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.