The situation as of C++23 seems to be (Godbolt): initializer_list
is a contiguous range, but not a view and not a borrowed range. This is counterintuitive, since in fact
initializer_list
is exactly a non-owning view over a range that lives somewhere else; copying an initializer_list
is always cheap and always "shallow not deep." So a priori you'd think it's a view
.
initializer_list
's internal state is exactly a pointer and a length and nothing else; it can safely be "deboned" into a pair of pointers. So a priori you'd think it's a "borrowed range."
My hypothesis is that the current wording makes sense primarily because initializer_list
is not actually a value type; it's more of a syntactic tag, like std::nullptr_t
or std::reference_wrapper
. We rarely pass around values of type initializer_list
. Instead, they pop into existence to represent a syntactic feature of the call-site. The actual type itself doesn't behave like a well-behaved C++20 Ranges range (if such a thing can be said to exist at all).
template<class R>
auto debone(R rg) {
static_assert(std::borrowed_range<R>);
return std::ranges::subrange(rg.begin(), rg.end());
}
auto x = debone<std::initializer_list<int>>({1,2,3,4});
// fails the static_assert -- good!
If this code didn't fail to compile, then the returned x
would contain a pair of iterators into a temporary array (the backing array of the initializer_list
) which has already been destroyed.
Other "parameter-only types" must deal with the same tradeoff. They generally do advertise borrowed-range-ness, and justify themselves by being a teeny tiny bit harder to construct accidentally from a temporary:
auto x = debone<std::string_view>("hello world");
// compiles quietly, but also happens to work
auto x = debone<std::string_view>("hello world"s);
// compiles quietly, and has UB: x.begin() dangles
auto x = debone<std::span<const int>>({{1,2,3}});
// compiles quietly, and has UB, but will likely work "in practice" (*)
auto x = debone<std::span<const int>>(std::vector{1,2,3}});
// compiles quietly, and has UB: x.begin() dangles
(* — See P2752 "Static storage for braced initializers", adopted as a DR in 2023, for why this will likely appear to work in practice. But it's still accessing the backing array outside of its actual lifetime.)
I hypothesize that span
and string_view
"correctly" advertise themselves as borrowed views because they expect to sometimes be used as value types rather than "parameter-only types" (this is also why they provide operator=
and swap
, for example). But initializer_list
is pretty much a "pure" parameter-only type — it has even been seriously proposed to eliminate its operator=
. initializer_list
doesn't expect ever to be passed to a Ranges algorithm on purpose. So it doesn't bother to advertise itself as a borrowed range or a view.