span’s
initializer_list constructor for C++29| Document #: | P4190R0 |
| Date: | 2026-04-16 |
| Project: | Programming Language C++ LEWG |
| Reply-to: |
Mark Hoemmen <mark.hoemmen-MILD-OBFUSCATION@gmail.com> Hana Dusíková <hanicka-MILD-OBFUSCATION@hanicka.net> Rob Parolin <robertoparolin-MILD-OBFUSCATION@gmail.com> |
P4144R1
reverted P2447R6
by removing span’s initializer_list
constructor. We propose to add it back in C++29, but with constraints
this time, in order to avoid the unfortunate conversions that led to its
removal.
P2447R6
proposed adding the following span constructor for
C++26.
constexpr explicit(extent != dynamic_extent)
span(std::initializer_list<value_type> il);As P4144R1 explains, the new constructor’s lack of constraints led to silent changes in behavior from C++23 to C++26. P2447 gave some examples, but the following example from P4144 motivated WG21 enough to reconsider the feature.
#include <span>
int main() {
bool data[4] = {true, false, true, false};
bool* ptr = data;
size_t size = 4;
std::span<bool> span_nc{ptr, size};
// OK in both C++23 and C++26
assert(span_nc.size() == size);
std::span<const bool> bad_span{ptr, size};
// OK in C++23, but FAILS in C++26
assert(bad_span.size() == size);
return 0;
}WG21 thus voted at the Croydon meeting in March 2026 to adopt P4144R1, which removed the new constructor entirely.
Coauthor Hana Dusíková came up with a way to constrain the
constructor generically (for all T) so that it refuses to
convert a pointer to value_type.
template <same_as<value_type> InitListValueType>
constexpr
my_span(std::initializer_list<InitListValueType> il)
requires (is_const_v<ElementType>);This would prohibit the troublesome cases discussed in P4144R1.
However, it would also change the original P2447R6 design by forcing the
type match to be exact. For example,
span<const bool>{true, false, true} would work, but
span<const bool>{1, 0, 1} would fail overload
resolution.
Adding the constructor is still a breaking change, as the following
two examples from P2447R6 show. The difference is that this proposal
would ensure that the common case of a pointer and a size would always
resolve to the span(It first, size_type count)
constructor.
void one(pair<int, int>); // 1
void one(span<const int>); // 2
void t1() { one({1, 2}); } // ambiguous between 1 and 2In C++26, one({1, 2}) would call the
pair<int, int> overload. After adoption of this
proposal, one({1, 2}) would be ambiguous. This is no
different than what adoption of P2447R6 for C++26 would have caused for
valid C++23 code.
void two(span<const int, 2>);
void t2() { two({{1, 2}}); } // ill-formed; previously well-formedIn C++26, two({{1, 2}}) would be well-formed. After
adoption of this proposal, two({{1, 2}}) would become
ill-formed. This is no different than what adoption of P2447R6 for C++26
would have caused for valid C++23 code.
We do not think the proposed change would affect overload resolution
for any of the other constructors. For example, the
span(It first, End last) constructor constrains
End not to be convertible to size_t, so
span(pointer, size) would never resolve to this anyway.
The constructor’s same_as<value_type> constraint
excludes some use cases that would have an unambiguous meaning. For
example:
span<const bool>{true, false, true} would
work, but span<const bool>{1, 0, 1} would fail
overload resolution; and
span<const float>{1.0f, 2.0f, 3.0f} would
work, but span<const float>{1, 2, 3} and
span<const float>{1.0, 2.0, 3.0} would fail overload
resolution.
One could imagine relaxing the constraint to permit conversions
between arithmetic types. More generally, one could imagine trying to
exclude all cases that might change overload resolution from any of the
existing constructors to the initializer_list
constructor.
We do not propose relaxing the constraint, because doing so would
overly complicate span. A span is a pointer
and a size. The more constructors we add to it, the less clear this
becomes.
NVIDIA’s libcu++ library is a Standard Library implementation. Pull Request 8314 implements the proposed change in libc++.
This Compiler Explorer
link shows a full span implementation with some tests.
Appendix A shows the implementation.
__cpp_lib_span_initializer_list macro[ Editor's note: In
[version.syn], add the __cpp_lib_span_initializer_list
feature test macro definition. Adjust the placeholder value as needed so
as to denote this proposal’s date of adoption. ]
#define __cpp_lib_span 202311L // freestanding, also in <span>#define __cpp_lib_span_initializer_list YYYMML // freestanding, also in <span>#define __cpp_lib_spanstream 202106L // also in <iosfwd>, <spanstream>initializer_list include to [span.syn][ Editor's note: Add
#include <initializer_list> to [span.syn] (“Header
<span> synopsis”) as shown below. ]
#include <initializer_list> // see [initializer.list.syn]// mostly freestanding
namespace std {initializer_list constructor to span’s class
declaration[ Editor's note: Add
span(initializer_list<value_type>) constructor to
span’s class declaration in [span.overview], as shown
below. ]
// [span.cons], constructors, copy, and assignment
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); constexpr span(const span& other) noexcept = default;
template<class OtherElementType, size_t OtherExtent>
constexpr explicit(see below)
span(const span<OtherElementType, OtherExtent>& s) noexcept;initializer_list constructor to span’s
description[ Editor's note: Add
span(initializer_list<value_type>) constructor
description to [span.cons]. ]
19
Effects: Initializes data_ with
ranges::data(r) and size_ with
ranges::size(r).
20
Throws: What and when ranges::data(r) and
ranges::size(r) throw.
template<class InitListValueType>
constexpr explicit(extent != dynamic_extent)
span(std::initializer_list<InitListValueType> il);21 Constraints:
22
Hardened preconditions: If extent is not equal to
dynamic_extent, then il.size() == extent is
true.
23
Effects: Initializes data_ with
il.data() and size_ with il.size().
constexpr span(const span& other) noexcept = default;24
Postconditions:
other.size() == size() && other.data() == data().
[ Editor's note: Add a new [diff.cpp26.containers] section, as shown below. ]
1 Affected subclause: [span.overview]
Change: span<const T> is
constructible from initializer_list<T>.
Rationale: Permit passing a braced initializer list
to a function taking span.
Effect on original feature: Valid C++ 2026 code that relies on the lack of this constructor may refuse to compile, or change behavior in this revision of C++.
[Example 1:
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— end example]
#include <any>
#include <cassert>
#include <stdexcept>
#include <iterator>
#include <span>
#define PROPOSED_CHANGE 1
namespace std2 {
using std::dynamic_extent;
using std::ptrdiff_t;
using std::size_t;
template<class ElementType, size_t Extent = dynamic_extent>
class span;
namespace impl {
template<class T>
constexpr bool is_span_v = false;
template<class ElementType, size_t Extent>
constexpr bool is_span_v<span<ElementType, Extent>> = true;
template<class ElementType, size_t Extent>
constexpr bool is_span_v<
std::span<ElementType, Extent>> = true;
template<class T>
constexpr bool is_std_array_v = false;
template<class ValueType, size_t Extent>
constexpr bool is_std_array_v<
std::array<ValueType, Extent>> = true;
} // namespace impl
template<class ElementType, size_t Extent>
class span {
private:
ElementType* data_ = nullptr;
size_t size_ = Extent == dynamic_extent ? size_t() : Extent;
public:
// constants and types
using element_type = ElementType;
using value_type = std::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 = pointer;
using const_iterator = const_pointer;
using reverse_iterator = std::reverse_iterator<iterator>;
// libc++ doesn't seem to define std::const_iterator or
// std::make_const_iterator in <iterator>. We don't actually
// need these, though.
using const_reverse_iterator =
std::reverse_iterator<const_iterator>;
static constexpr size_type extent = Extent;
// [span.cons], constructors, copy, and assignment
constexpr span() noexcept
requires(Extent == dynamic_extent || Extent == 0) = default;
template<class It>
requires(
std::contiguous_iterator<It> &&
std::is_convertible_v<
std::remove_reference_t<std::iter_reference_t<It>>(*)[],
element_type(*)[]
>
)
constexpr explicit(extent != dynamic_extent)
span(It first, size_type count)
: data_(::std::to_address(first)), size_(count) {}
template<class It, class End>
requires(
std::is_convertible_v<
std::remove_reference_t<std::iter_reference_t<It>>(*)[],
element_type(*)[]
> &&
std::contiguous_iterator<
std::remove_reference_t<std::iter_reference_t<It>>
> &&
std::sized_sentinel_for<End, It> &&
! std::is_convertible_v<End, size_t>
)
constexpr explicit(extent != dynamic_extent)
span(It first, End last)
: data_(::std::to_address(first)),
size_(last - first)
{}
template<size_t N>
constexpr span(
std::type_identity_t<element_type> (&arr)[N]) noexcept
requires(
(extent == dynamic_extent || N == extent) &&
std::is_convertible_v<
std::remove_pointer_t<decltype(std::data(arr))>(*)[],
element_type(*)[]
>
) : data_(arr), size_(N) {}
template<class T, size_t N>
constexpr span(std::array<T, N>& arr) noexcept
requires(
(extent == dynamic_extent || N == extent) &&
std::is_convertible_v<
std::remove_pointer_t<decltype(std::data(arr))>(*)[],
element_type(*)[]
>
) : data_(arr.data()), size_(N) {}
template<class T, size_t N>
constexpr span(const std::array<T, N>& arr) noexcept
requires(
(extent == dynamic_extent || N == extent) &&
std::is_convertible_v<
std::remove_pointer_t<decltype(std::data(arr))>(*)[],
element_type(*)[]
>
) : data_(arr.data()), size_(N) {}
template<class R> requires(
std::ranges::contiguous_range<
std::remove_reference_t<
std::ranges::range_reference_t<R>
>
> &&
std::ranges::sized_range<
std::remove_reference_t<
std::ranges::range_reference_t<R>
>
> &&
(
std::ranges::borrowed_range<
std::remove_reference_t<
std::ranges::range_reference_t<R>
>
> ||
std::is_const_v<element_type>
) &&
! impl::is_span_v<std::remove_cvref_t<R>> &&
! impl::is_std_array_v<std::remove_cvref_t<R>> &&
! std::is_array_v<std::remove_cvref_t<R>> &&
std::is_convertible_v<
std::remove_reference_t<
std::ranges::range_reference_t<R>
>(*)[],
element_type(*)[]
>
)
constexpr explicit(extent != dynamic_extent)
span(R&& r)
: data_( ::std::ranges::data(r)),
size_( ::std::ranges::size(r))
{}
#if defined(PROPOSED_CHANGE)
template<std::same_as<value_type> InitListValueType>
constexpr explicit(extent != dynamic_extent)
span( ::std::initializer_list<InitListValueType> il)
requires(std::is_const_v<element_type>)
: data_(std::data(il)), size_(il.size())
{
assert(size_ == 3);
}
#endif
constexpr span(const span& other) noexcept = default;
template<class OtherElementType, size_t OtherExtent>
requires(
extent == dynamic_extent ||
OtherExtent == dynamic_extent ||
extent == OtherExtent
)
constexpr
explicit(
extent != dynamic_extent &&
OtherExtent == dynamic_extent
)
span(const span<OtherElementType, OtherExtent>& s) noexcept
: data_(s.data()), size_(s.size())
{}
constexpr span& operator=(const span& other) noexcept = default;
// [span.sub], subviews
template<size_t Count>
constexpr span<element_type, Count> first() const {
return span<element_type, Count>(data(), Count);
}
template<size_t Count>
constexpr span<element_type, Count> last() const {
return span<element_type, Count>(
data() + (size() - Count),
Count
);
}
template<size_t Offset, size_t Count = dynamic_extent>
constexpr span<element_type,
Count != dynamic_extent ? Count
: (Extent != dynamic_extent ? Extent - Offset
: dynamic_extent)
> subspan() const {
static_assert(
Offset <= Extent &&
(Count == dynamic_extent || Count <= Extent - Offset)
);
static constexpr size_t ResultExtent =
Count != dynamic_extent ? Count
: (Extent != dynamic_extent ? Extent - Offset
: dynamic_extent);
return span<ElementType, ResultExtent>(
data() + Offset,
Count != dynamic_extent ? Count : size() - Offset
);
}
constexpr span<element_type, dynamic_extent>
first(size_type count) const {
return span<element_type, dynamic_extent>(data(), count);
}
constexpr span<element_type, dynamic_extent>
last(size_type count) const {
return span<element_type, dynamic_extent>(
data() + (size() - count),
count
);
}
constexpr span<element_type, dynamic_extent> subspan(
size_type offset, size_type count = dynamic_extent) const {
return span<element_type, dynamic_extent>(
data() + offset,
count == dynamic_extent ? size() - offset : count
);
}
// [span.obs], observers
constexpr size_type size() const noexcept {
return Extent == dynamic_extent ? size_ : Extent;
}
constexpr size_type size_bytes() const noexcept {
return size() * sizeof(element_type);
}
constexpr bool empty() const noexcept {
return size() == 0;
}
// [span.elem], element access
constexpr reference operator[] (size_type idx) const {
return data_[idx];
}
constexpr reference at(size_type idx) const {
if (idx >= size()) {
throw std::out_of_range("span::at: Index out of bounds");
}
return data_[idx];
}
constexpr reference front() const {
return *data();
}
constexpr reference back() const {
return *(data() + (size() - 1));
}
constexpr pointer data() const noexcept {
return data_;
}
// [span.iterators], iterator support
constexpr iterator begin() const noexcept {
return data();
}
constexpr iterator end() const noexcept {
return data() + size();
}
constexpr const_iterator cbegin() const noexcept {
return begin();
}
constexpr const_iterator cend() const noexcept {
return end();
}
constexpr reverse_iterator rbegin() const noexcept {
return reverse_iterator(end());
}
constexpr reverse_iterator rend() const noexcept {
return reverse_iterator(begin());
}
constexpr const_reverse_iterator
crbegin() const noexcept {
return rbegin();
}
constexpr const_reverse_iterator
crend() const noexcept {
return rend();
}
};
// [span.objectrep], views of object representation
template<class ElementType, size_t Extent>
requires(
! ::std::is_volatile_v<ElementType>
)
span<
const std::byte,
Extent == dynamic_extent ? dynamic_extent : sizeof(ElementType) * Extent
>
as_bytes(span<ElementType, Extent> s) noexcept {
using R = span<
const std::byte,
Extent == dynamic_extent ? dynamic_extent : sizeof(ElementType) * Extent
>;
return R(
reinterpret_cast<const std::byte*>(s.data()),
s.size_bytes()
);
}
template<class ElementType, size_t Extent>
requires(
! ::std::is_const_v<ElementType> &&
! ::std::is_volatile_v<ElementType>
)
span<
std::byte,
Extent == dynamic_extent ? dynamic_extent : sizeof(ElementType) * Extent
>
as_writable_bytes(span<ElementType, Extent> s) noexcept {
using R = span<
std::byte,
Extent == dynamic_extent ? dynamic_extent : sizeof(ElementType) * Extent
>;
return R(
reinterpret_cast<std::byte*>(s.data()),
s.size_bytes()
);
}
} // namespace std2
namespace std::ranges {
template<class ElementType, size_t Extent>
constexpr bool enable_view<
::std2::span<ElementType, Extent>
> = true;
template<class ElementType, size_t Extent>
constexpr bool enable_borrowed_range<
::std2::span<ElementType, Extent>
> = true;
}
bool bool_true() { return true; }
bool bool_false() { return false; }
int main() {
#if defined(PROPOSED_CHANGE)
{
std2::span<const bool> b{true, false, true};
assert(b.size() == 3);
assert(b[0] && ! b[1] && b[2]);
}
{
std2::span<const bool> b{bool_true(), bool_false(), bool_true()};
assert(b.size() == 3);
assert(b[0] && ! b[1] && b[2]);
}
//{
// std2::span<const bool> b{1, 0, 1};
// assert(b.size() == 3);
// assert(b[0] && ! b[1] && b[2]);
//}
//{
// std2::span<const std::any> b{"foo", true, 1.25f};
// assert(b.size() == 3);
//}
{
std2::span<const std::any> b{
std::any{"foo"}, std::any{true}, std::any{1.25f}
};
assert(b.size() == 3);
}
{
std2::span<const float> b{1.0f, 2.0f, 3.0f};
assert(b.size() == 3);
assert(b[0] == 1.0f && b[1] == 2.0f && b[2] == 3.0f);
}
//{
// std2::span<const float> b{1.0, 2.0, 3.0};
// assert(b.size() == 3);
// assert(b[0] == 1.0f && b[1] == 2.0f && b[2] == 3.0f);
//}
//{
// std2::span<const float> b{1, 2, 3};
// assert(b.size() == 3);
// assert(b[0] == 1.0f && b[1] == 2.0f && b[2] == 3.0f);
//}
#endif
return 0;
}