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:
isis_const_v < element_type > true.Preconditions: If
is not equal toextent , thendynamic_extent is equal toil . size () .extent Effects: Initializes
withdata_ andil . begin () withsize_ .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 fromspan < 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.