1. Changelog
-
R5 (post-LEWG 2023):
-
Reintroduce feature-test macro
.__cpp_lib_span_initializer_list -
Add Annex C entries to § 6 Proposed wording; LEWG points out that it’s easy for LWG to eliminate insertions they deem redundant.
-
-
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-test 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
std :: span < const int > v = {{ 1 , 2 , 3 }}; // dangles
then why not call that "good enough"? Why do we need to be able to use a single set of braces?
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
std :: vector < int > v = {{ 1 , 2 , 3 }};
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.)
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], adopted as a DR at Varna 2023, allows a constant
like
to 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 in static storage.
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 }}); }
|
| |
Before 2752 | Array on stack | Ill-formed |
---|---|---|
Today | Array on stack | Ill-formed |
P2447 | 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) in either column is to adopt P2447, which by a happy coincidence also permits the single-braced form.
4. Breaking changes
This change will, of course, break some code (most of it pathological).
We propose adding three new examples to Annex C.
But any change to overload sets can break code, and sometimes LWG doesn’t bother with
an Annex C entry.
For example, C++23 adopted [P1425] "Iterator-pair constructors for
and
"
with no change to Annex C, despite its breaking code like this:
void zero ( queue < int > ); void zero ( pair < int * , int *> ); int a [ 10 ]; void test () { zero ({ a , a + 10 }); }
Before: Calls
.
After P1425: Ambiguous.
To fix: Eliminate the ambiguous overloading, or cast the argument to
.
Therefore, we’re happy for LWG to eliminate any or all of our proposed Annex C entries if they’re going too far into the weeds.
For explanation and suggested fixits for each of the Annex C examples included in § 6 Proposed wording, see P2447R4 §4.
5. Straw polls
P2447R4 was presented to LEWG on 2023-09-12. The following polls were taken. The first was classified as "no consensus," the second as "weak consensus."
SF | F | N | A | SA | |
---|---|---|---|---|---|
Forward P2447R4 to LWG for C++26 and as a defect. | 2 | 5 | 3 | 2 | 1 |
Forward P2447R4 to LWG for C++26 (not as a defect). | 2 | 6 | 4 | 1 | 1 |
6. 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 ()
Modify [diff.cpp26] as follows:
Note: For explanation and suggested fixits for each of these examples, see P2447R4 §4. My understanding is that Annex C wording shouldn’t contain that extra material.
[containers]: containers library
1․ Affected subclause: [span.overview]
Change:is constructible from
span < const T > .
initializer_list < T >
Rationale: Permit passing a braced initializer list to a function taking.
span
Effect on original feature: Valid C++ 2023 code that relies on the lack of this constructor may refuse to compile, or change behavior. For example:void one ( pair < int , int > ); // #1 void one ( span < const int > ); // #2 void t1 () { one ({ 1 , 2 }); } // ambiguous between #1 and #2; previously called #1 void two ( span < const int , 2 > ); void t2 () { two ({{ 1 , 2 }}); } // ill-formed; previously well-formed void * a [ 10 ]; int x = span < void * const > { a , 0 }. size (); // x is 2; previously 0 any b [ 10 ]; int y = span < const any > { b , b + 10 }. size (); // y is 2; previously 10
Add a feature-test macro to [version.syn]/2 as follows:
#define __cpp_lib_span 202002L // also in <span> #define __cpp_lib_span_initializer_list XXYYZZL // also in <span> #define __cpp_lib_spanstream 202106L // also in <spanstream>
7. Acknowledgments
-
Thanks to Federico Kircheis for writing the first drafts of this paper.
-
Thanks to Jarrad Waterloo for his support.