The short answer is that std::initializer_list<T>
is used to create a new range, for the purposes of initialization. While std::span<T>
is used to refer to existing ranges, for better APIs.
std::initializer_list<T>
is a language feature that actually constructs a new array and owns it. It solves the problem of how to conveniently initialize containers:
template <typename T>
struct vector {
vector(std::initializer_list<T>);
};
vector<int> v = {1, 2, 3, 4};
That creates a std::initializer_list<int>
on the fly containing the four integers there, and passes it into vector
so that it can construct itself properly.
This is really the only place std::initializer_list<T>
should be used: either constructors or function parameters to pass a range in on the fly, for convenience (unit tests are a common place that desires such convenience).
std::span<T>
on the other hand is used to refer to an existing range. Its job is to replace functions of the form:
void do_something(int*, size_t);
with
void do_something(std::span<int>);
Which makes such functions generally easier to use and safer. std::span
is constructible from any appropriate contiguous range, so taking our example from earlier:
std::vector<int> v = {1, 2, 3, 4};
do_something(v); // ok
It can also be used to replace functions of the form:
void do_something_else(std::vector<unsigned char> const&);
Which can only be called with, specifically, a vector
, with the more general:
void do_something_else(std::span<unsigned char const>);
Which can be called with any backing contiguous storage over unsigned char
.
With span
you have to be careful, since it's basically a reference that just doesn't get spelled with a &
, but it is an extremely useful type.